@morscherlab/mld-sdk 0.9.5 → 0.9.7

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 (38) hide show
  1. package/dist/components/AutoGroupModal.vue.d.ts +118 -0
  2. package/dist/components/AutoGroupModal.vue.js.map +1 -1
  3. package/dist/components/DataFrame.vue.js +1 -1
  4. package/dist/components/DataFrame.vue.js.map +1 -1
  5. package/dist/components/ExperimentPopover.vue.d.ts +5 -0
  6. package/dist/components/ExperimentPopover.vue.js +86 -46
  7. package/dist/components/ExperimentPopover.vue.js.map +1 -1
  8. package/dist/components/ExperimentSelectorModal.vue.js +1 -1
  9. package/dist/components/ExperimentSelectorModal.vue.js.map +1 -1
  10. package/dist/components/FileUploader.vue.js +13 -10
  11. package/dist/components/FileUploader.vue.js.map +1 -1
  12. package/dist/components/FormBuilder.vue.d.ts +287 -0
  13. package/dist/components/StepWizard.vue.d.ts +1 -1
  14. package/dist/components/StepWizard.vue.js.map +1 -1
  15. package/dist/composables/useApi.js +26 -12
  16. package/dist/composables/useApi.js.map +1 -1
  17. package/dist/composables/useAuth.js +7 -6
  18. package/dist/composables/useAuth.js.map +1 -1
  19. package/dist/composables/useExperimentSelector.js +14 -5
  20. package/dist/composables/useExperimentSelector.js.map +1 -1
  21. package/dist/composables/useForm.js +2 -2
  22. package/dist/composables/useForm.js.map +1 -1
  23. package/dist/composables/usePlatformContext.js +11 -0
  24. package/dist/composables/usePlatformContext.js.map +1 -1
  25. package/dist/styles.css +209 -100
  26. package/package.json +1 -1
  27. package/src/components/AutoGroupModal.vue +1 -1
  28. package/src/components/DataFrame.vue +1 -1
  29. package/src/components/ExperimentPopover.vue +45 -19
  30. package/src/components/ExperimentSelectorModal.vue +1 -1
  31. package/src/components/FileUploader.vue +14 -11
  32. package/src/components/StepWizard.vue +1 -1
  33. package/src/composables/useApi.ts +33 -17
  34. package/src/composables/useAuth.ts +11 -8
  35. package/src/composables/useExperimentSelector.ts +19 -7
  36. package/src/composables/useForm.ts +3 -3
  37. package/src/composables/usePlatformContext.ts +11 -1
  38. package/src/styles/components/experiment-popover.css +110 -50
@@ -72,7 +72,15 @@ function useExperimentSelector(options = {}) {
72
72
  } else {
73
73
  experiments.value = [...experiments.value, ...filtered];
74
74
  }
75
- total.value = effectiveType || !(allowedTypes == null ? void 0 : allowedTypes.length) ? data.total : filtered.length;
75
+ if (!effectiveType && allowedTypes && allowedTypes.length > 1) {
76
+ if (data.experiments.length < limit) {
77
+ total.value = experiments.value.length;
78
+ } else {
79
+ total.value = experiments.value.length + 1;
80
+ }
81
+ } else {
82
+ total.value = data.total;
83
+ }
76
84
  } catch (e) {
77
85
  error.value = e instanceof Error ? e.message : "Failed to fetch experiments";
78
86
  if (page.value === 0) {
@@ -84,6 +92,7 @@ function useExperimentSelector(options = {}) {
84
92
  }
85
93
  }
86
94
  async function fetchFilterOptions() {
95
+ var _a;
87
96
  if (filterOptionsFetched) return;
88
97
  filterOptionsFetched = true;
89
98
  const base = platformBase ?? "/api";
@@ -93,13 +102,13 @@ function useExperimentSelector(options = {}) {
93
102
  ]);
94
103
  if (typesRes.status === "fulfilled" && Array.isArray(typesRes.value)) {
95
104
  experimentTypes.value = typesRes.value.map((t) => ({
96
- value: t.name,
97
- label: t.name,
105
+ value: t.value,
106
+ label: t.label,
98
107
  color: t.color
99
108
  }));
100
109
  }
101
- if (projectsRes.status === "fulfilled" && Array.isArray(projectsRes.value)) {
102
- projects.value = projectsRes.value.map((p) => ({
110
+ if (projectsRes.status === "fulfilled" && ((_a = projectsRes.value) == null ? void 0 : _a.projects) && Array.isArray(projectsRes.value.projects)) {
111
+ projects.value = projectsRes.value.projects.map((p) => ({
103
112
  value: p.name,
104
113
  label: p.name
105
114
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"useExperimentSelector.js","sources":["../../src/composables/useExperimentSelector.ts"],"sourcesContent":["import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n experiments: Ref<ExperimentSummary[]>\n total: Ref<number>\n selectedExperiment: Ref<ExperimentSummary | null>\n filters: ExperimentFilters\n isLoading: Ref<boolean>\n error: Ref<string | null>\n page: Ref<number>\n hasMore: ComputedRef<boolean>\n // Sort\n sortKey: Ref<string>\n // Filter options\n experimentTypes: Ref<ExperimentTypeOption[]>\n projects: Ref<SelectOption<string>[]>\n // Grouped view\n groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>\n // Methods\n fetch: () => Promise<void>\n loadMore: () => Promise<void>\n reset: () => void\n select: (experiment: ExperimentSummary) => void\n clear: () => void\n fetchFilterOptions: () => Promise<void>\n}\n\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n const page = ref(0)\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n })\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n async function fetchExperiments(): Promise<void> {\n isLoading.value = true\n error.value = null\n try {\n const params = new URLSearchParams()\n // Priority: explicit option > platform context (single type) > filter dropdown > no filter\n const allowedTypes = getPlatformContext()?.allowedExperimentTypes\n const effectiveType = experimentType\n ?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)\n ?? filters.experimentType\n ?? undefined\n if (effectiveType) params.set('experiment_type', effectiveType)\n if (filters.status) params.set('status', filters.status)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n\n // Sort params\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n // Date preset → created_after\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n\n params.set('limit', String(limit))\n params.set('skip', String(page.value * limit))\n\n const query = params.toString()\n const base = platformBase ?? '/api'\n const url = `${base}/experiments${query ? `?${query}` : ''}`\n const data = await api.get<ExperimentListResponse>(url)\n\n // Client-side filter for multiple allowed types (backend only supports single type)\n let filtered = data.experiments\n if (!effectiveType && allowedTypes && allowedTypes.length > 1) {\n const typeSet = new Set(allowedTypes)\n filtered = filtered.filter(e => typeSet.has(e.experiment_type))\n }\n\n if (page.value === 0) {\n experiments.value = filtered\n } else {\n experiments.value = [...experiments.value, ...filtered]\n }\n total.value = effectiveType || !allowedTypes?.length ? data.total : filtered.length\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'\n if (page.value === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n isLoading.value = false\n }\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? '/api'\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ name: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<Array<{ id: number; name: string }>>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n experimentTypes.value = typesRes.value.map(t => ({\n value: t.name,\n label: t.name,\n color: t.color,\n }))\n }\n\n if (projectsRes.status === 'fulfilled' && Array.isArray(projectsRes.value)) {\n projects.value = projectsRes.value.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n }\n\n // Group experiments by project (client-side)\n const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {\n const groups = new Map<string, ExperimentSummary[]>()\n for (const exp of experiments.value) {\n const key = exp.project_name ?? exp.project ?? 'No project'\n const list = groups.get(key)\n if (list) {\n list.push(exp)\n } else {\n groups.set(key, [exp])\n }\n }\n // Sort alphabetically, \"No project\" last\n return [...groups.entries()].sort(([a], [b]) => {\n if (a === 'No project') return 1\n if (b === 'No project') return -1\n return a.localeCompare(b)\n })\n })\n\n // Debounced watch on search filter\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n watch(\n () => filters.search,\n () => {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n page.value = 0\n fetchExperiments()\n }, 300)\n },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],\n () => {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = null\n page.value = 0\n fetchExperiments()\n },\n )\n\n onScopeDispose(() => {\n if (debounceTimer) clearTimeout(debounceTimer)\n })\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n groupedByProject,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useExperimentSelector.js","sources":["../../src/composables/useExperimentSelector.ts"],"sourcesContent":["import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n experiments: Ref<ExperimentSummary[]>\n total: Ref<number>\n selectedExperiment: Ref<ExperimentSummary | null>\n filters: ExperimentFilters\n isLoading: Ref<boolean>\n error: Ref<string | null>\n page: Ref<number>\n hasMore: ComputedRef<boolean>\n // Sort\n sortKey: Ref<string>\n // Filter options\n experimentTypes: Ref<ExperimentTypeOption[]>\n projects: Ref<SelectOption<string>[]>\n // Grouped view\n groupedByProject: ComputedRef<[string, ExperimentSummary[]][]>\n // Methods\n fetch: () => Promise<void>\n loadMore: () => Promise<void>\n reset: () => void\n select: (experiment: ExperimentSummary) => void\n clear: () => void\n fetchFilterOptions: () => Promise<void>\n}\n\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n const page = ref(0)\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n })\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n async function fetchExperiments(): Promise<void> {\n isLoading.value = true\n error.value = null\n try {\n const params = new URLSearchParams()\n // Priority: explicit option > platform context (single type) > filter dropdown > no filter\n const allowedTypes = getPlatformContext()?.allowedExperimentTypes\n const effectiveType = experimentType\n ?? (allowedTypes?.length === 1 ? allowedTypes[0] : undefined)\n ?? filters.experimentType\n ?? undefined\n if (effectiveType) params.set('experiment_type', effectiveType)\n if (filters.status) params.set('status', filters.status)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n\n // Sort params\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n // Date preset → created_after\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n\n params.set('limit', String(limit))\n params.set('skip', String(page.value * limit))\n\n const query = params.toString()\n const base = platformBase ?? '/api'\n const url = `${base}/experiments${query ? `?${query}` : ''}`\n const data = await api.get<ExperimentListResponse>(url)\n\n // Client-side filter for multiple allowed types (backend only supports single type)\n let filtered = data.experiments\n if (!effectiveType && allowedTypes && allowedTypes.length > 1) {\n const typeSet = new Set(allowedTypes)\n filtered = filtered.filter(e => typeSet.has(e.experiment_type))\n }\n\n if (page.value === 0) {\n experiments.value = filtered\n } else {\n experiments.value = [...experiments.value, ...filtered]\n }\n // When client-side filtering is active (multiple allowedTypes), we can't\n // use data.total since it counts all types. Check if server has more pages.\n if (!effectiveType && allowedTypes && allowedTypes.length > 1) {\n if (data.experiments.length < limit) {\n // Server returned less than a full page — no more data\n total.value = experiments.value.length\n } else {\n // Might be more pages on the server\n total.value = experiments.value.length + 1\n }\n } else {\n total.value = data.total\n }\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'\n if (page.value === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n isLoading.value = false\n }\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? '/api'\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n experimentTypes.value = typesRes.value.map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n }\n\n // Group experiments by project (client-side)\n const groupedByProject = computed<[string, ExperimentSummary[]][]>(() => {\n const groups = new Map<string, ExperimentSummary[]>()\n for (const exp of experiments.value) {\n const key = exp.project_name ?? exp.project ?? 'No project'\n const list = groups.get(key)\n if (list) {\n list.push(exp)\n } else {\n groups.set(key, [exp])\n }\n }\n // Sort alphabetically, \"No project\" last\n return [...groups.entries()].sort(([a], [b]) => {\n if (a === 'No project') return 1\n if (b === 'No project') return -1\n return a.localeCompare(b)\n })\n })\n\n // Debounced watch on search filter\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\n watch(\n () => filters.search,\n () => {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n page.value = 0\n fetchExperiments()\n }, 300)\n },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, sortKey.value],\n () => {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = null\n page.value = 0\n fetchExperiments()\n },\n )\n\n onScopeDispose(() => {\n if (debounceTimer) clearTimeout(debounceTimer)\n })\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n groupedByProject,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n }\n}\n"],"names":[],"mappings":"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"}
@@ -54,8 +54,8 @@ const validators = {
54
54
  }
55
55
  };
56
56
  function useForm(initialValues, rules = {}) {
57
- const _initialValues = { ...initialValues };
58
- const data = reactive({ ...initialValues });
57
+ const _initialValues = structuredClone(initialValues);
58
+ const data = reactive(structuredClone(initialValues));
59
59
  const errors = reactive(
60
60
  Object.keys(initialValues).reduce((acc, key) => {
61
61
  acc[key] = null;
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.js","sources":["../../src/composables/useForm.ts"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref } from 'vue'\n\n/**\n * Validation rule function type.\n * Returns error message string if invalid, undefined/null if valid.\n */\nexport type ValidationRule<T = unknown> = (value: T, formData: Record<string, unknown>) => string | undefined | null\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldRules<T = unknown> {\n required?: boolean | string\n minLength?: number | { value: number; message: string }\n maxLength?: number | { value: number; message: string }\n min?: number | { value: number; message: string }\n max?: number | { value: number; message: string }\n pattern?: RegExp | { value: RegExp; message: string }\n email?: boolean | string\n custom?: ValidationRule<T> | ValidationRule<T>[]\n}\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldState {\n value: unknown\n error: string | null\n touched: boolean\n dirty: boolean\n}\n\n/**\n * Form state and methods.\n */\nexport interface UseFormReturn<T extends Record<string, unknown>> {\n // Form data (reactive)\n data: T\n\n // Field errors\n errors: Record<string, string | null>\n\n // Field touched state\n touched: Record<string, boolean>\n\n // Field dirty state (value changed from initial)\n dirty: Record<string, boolean>\n\n // Overall form state\n isValid: Ref<boolean>\n isDirty: Ref<boolean>\n isSubmitting: Ref<boolean>\n\n // Methods\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void\n setFieldError: (field: string, error: string | null) => void\n setFieldTouched: (field: string, touched?: boolean) => void\n validateField: (field: string) => boolean\n validate: () => boolean\n reset: (values?: Partial<T>) => void\n handleSubmit: (onSubmit: (data: T) => Promise<void> | void) => (e?: Event) => Promise<void>\n getFieldProps: <K extends keyof T>(field: K) => {\n modelValue: T[K]\n 'onUpdate:modelValue': (value: T[K]) => void\n onBlur: () => void\n error: string | null\n }\n}\n\n// Built-in validators\nconst validators = {\n required: (value: unknown, message = 'This field is required'): string | null => {\n if (value === null || value === undefined || value === '') {\n return message\n }\n if (Array.isArray(value) && value.length === 0) {\n return message\n }\n return null\n },\n\n minLength: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length < min) {\n return message || `Must be at least ${min} characters`\n }\n return null\n },\n\n maxLength: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length > max) {\n return message || `Must be at most ${max} characters`\n }\n return null\n },\n\n min: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value < min) {\n return message || `Must be at least ${min}`\n }\n return null\n },\n\n max: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value > max) {\n return message || `Must be at most ${max}`\n }\n return null\n },\n\n pattern: (value: unknown, pattern: RegExp, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (!pattern.test(value)) {\n return message || 'Invalid format'\n }\n return null\n },\n\n email: (value: unknown, message = 'Invalid email address'): string | null => {\n if (typeof value !== 'string' || !value) return null\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n if (!emailRegex.test(value)) {\n return message\n }\n return null\n },\n}\n\n/**\n * Form state management composable with validation.\n *\n * @param initialValues - Initial form values\n * @param rules - Validation rules for each field\n *\n * @example\n * ```typescript\n * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(\n * { email: '', password: '' },\n * {\n * email: { required: true, email: true },\n * password: { required: true, minLength: 8 },\n * }\n * )\n *\n * // In template\n * <BaseInput v-bind=\"getFieldProps('email')\" label=\"Email\" />\n * <BaseInput v-bind=\"getFieldProps('password')\" type=\"password\" label=\"Password\" />\n * <BaseButton @click=\"handleSubmit(onSubmit)\" :disabled=\"!isValid\">Submit</BaseButton>\n * ```\n */\nexport function useForm<T extends Record<string, unknown>>(\n initialValues: T,\n rules: Partial<Record<keyof T, FieldRules>> = {}\n): UseFormReturn<T> {\n // Store initial values for reset\n const _initialValues = { ...initialValues }\n\n // Reactive form data\n const data = reactive({ ...initialValues }) as T\n\n // Field state - use simple Record types for better TS compatibility\n const errors = reactive<Record<string, string | null>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = null\n return acc\n }, {} as Record<string, string | null>)\n )\n\n const touched = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const dirty = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const isSubmitting = ref(false)\n\n // Watch data changes to track dirty state\n watch(\n () => ({ ...data }),\n (newData) => {\n for (const key of Object.keys(newData)) {\n dirty[key] = newData[key as keyof T] !== _initialValues[key as keyof T]\n }\n },\n { deep: true }\n )\n\n // Validate a single field\n function validateField(field: string): boolean {\n const value = data[field as keyof T]\n const fieldRules = rules[field as keyof T]\n\n if (!fieldRules) {\n errors[field] = null\n return true\n }\n\n // Check required\n if (fieldRules.required) {\n const message = typeof fieldRules.required === 'string' ? fieldRules.required : undefined\n const error = validators.required(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Skip other validations if empty and not required\n if (value === null || value === undefined || value === '') {\n errors[field] = null\n return true\n }\n\n // Check minLength\n if (fieldRules.minLength !== undefined) {\n const config = typeof fieldRules.minLength === 'number'\n ? { value: fieldRules.minLength, message: undefined }\n : fieldRules.minLength\n const error = validators.minLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check maxLength\n if (fieldRules.maxLength !== undefined) {\n const config = typeof fieldRules.maxLength === 'number'\n ? { value: fieldRules.maxLength, message: undefined }\n : fieldRules.maxLength\n const error = validators.maxLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check min\n if (fieldRules.min !== undefined) {\n const config = typeof fieldRules.min === 'number'\n ? { value: fieldRules.min, message: undefined }\n : fieldRules.min\n const error = validators.min(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check max\n if (fieldRules.max !== undefined) {\n const config = typeof fieldRules.max === 'number'\n ? { value: fieldRules.max, message: undefined }\n : fieldRules.max\n const error = validators.max(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check pattern\n if (fieldRules.pattern !== undefined) {\n const config = fieldRules.pattern instanceof RegExp\n ? { value: fieldRules.pattern, message: undefined }\n : fieldRules.pattern\n const error = validators.pattern(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check email\n if (fieldRules.email) {\n const message = typeof fieldRules.email === 'string' ? fieldRules.email : undefined\n const error = validators.email(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check custom validators\n if (fieldRules.custom) {\n const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom]\n for (const rule of customRules) {\n const error = rule(value, data as Record<string, unknown>)\n if (error) {\n errors[field] = error\n return false\n }\n }\n }\n\n errors[field] = null\n return true\n }\n\n // Validate all fields\n function validate(): boolean {\n let isAllValid = true\n for (const field of Object.keys(data)) {\n if (!validateField(field)) {\n isAllValid = false\n }\n }\n return isAllValid\n }\n\n // Computed overall validity\n const isValid = computed(() => {\n return Object.values(errors).every(error => error === null)\n })\n\n // Computed overall dirty state\n const isDirty = computed(() => {\n return Object.values(dirty).some(d => d)\n })\n\n // Set field value\n function setFieldValue<K extends keyof T>(field: K, value: T[K]): void {\n ;(data as Record<string, unknown>)[field as string] = value\n if (touched[field as string]) {\n validateField(field as string)\n }\n }\n\n // Set field error\n function setFieldError(field: string, error: string | null): void {\n errors[field] = error\n }\n\n // Set field touched\n function setFieldTouched(field: string, isTouched = true): void {\n touched[field] = isTouched\n if (isTouched) {\n validateField(field)\n }\n }\n\n // Reset form\n function reset(values?: Partial<T>): void {\n const resetValues = values ? { ..._initialValues, ...values } : _initialValues\n for (const key of Object.keys(data)) {\n ;(data as Record<string, unknown>)[key] = resetValues[key as keyof T]\n errors[key] = null\n touched[key] = false\n dirty[key] = false\n }\n }\n\n // Handle submit\n function handleSubmit(onSubmit: (data: T) => Promise<void> | void) {\n return async (e?: Event): Promise<void> => {\n e?.preventDefault()\n\n // Mark all fields as touched\n for (const field of Object.keys(data)) {\n touched[field] = true\n }\n\n if (!validate()) {\n return\n }\n\n isSubmitting.value = true\n try {\n await onSubmit(data)\n } finally {\n isSubmitting.value = false\n }\n }\n }\n\n // Get props for a field (for v-bind)\n function getFieldProps<K extends keyof T>(field: K) {\n const fieldStr = field as string\n return {\n modelValue: data[field],\n 'onUpdate:modelValue': (value: T[K]) => setFieldValue(field, value),\n onBlur: () => setFieldTouched(fieldStr),\n error: touched[fieldStr] ? errors[fieldStr] : null,\n }\n }\n\n return {\n data,\n errors,\n touched,\n dirty,\n isValid,\n isDirty,\n isSubmitting,\n setFieldValue,\n setFieldError,\n setFieldTouched,\n validateField,\n validate,\n reset,\n handleSubmit,\n getFieldProps,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useForm.js","sources":["../../src/composables/useForm.ts"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref } from 'vue'\n\n/**\n * Validation rule function type.\n * Returns error message string if invalid, undefined/null if valid.\n */\nexport type ValidationRule<T = unknown> = (value: T, formData: Record<string, unknown>) => string | undefined | null\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldRules<T = unknown> {\n required?: boolean | string\n minLength?: number | { value: number; message: string }\n maxLength?: number | { value: number; message: string }\n min?: number | { value: number; message: string }\n max?: number | { value: number; message: string }\n pattern?: RegExp | { value: RegExp; message: string }\n email?: boolean | string\n custom?: ValidationRule<T> | ValidationRule<T>[]\n}\n\n/**\n * Field validation rules configuration.\n */\nexport interface FieldState {\n value: unknown\n error: string | null\n touched: boolean\n dirty: boolean\n}\n\n/**\n * Form state and methods.\n */\nexport interface UseFormReturn<T extends Record<string, unknown>> {\n // Form data (reactive)\n data: T\n\n // Field errors\n errors: Record<string, string | null>\n\n // Field touched state\n touched: Record<string, boolean>\n\n // Field dirty state (value changed from initial)\n dirty: Record<string, boolean>\n\n // Overall form state\n isValid: Ref<boolean>\n isDirty: Ref<boolean>\n isSubmitting: Ref<boolean>\n\n // Methods\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void\n setFieldError: (field: string, error: string | null) => void\n setFieldTouched: (field: string, touched?: boolean) => void\n validateField: (field: string) => boolean\n validate: () => boolean\n reset: (values?: Partial<T>) => void\n handleSubmit: (onSubmit: (data: T) => Promise<void> | void) => (e?: Event) => Promise<void>\n getFieldProps: <K extends keyof T>(field: K) => {\n modelValue: T[K]\n 'onUpdate:modelValue': (value: T[K]) => void\n onBlur: () => void\n error: string | null\n }\n}\n\n// Built-in validators\nconst validators = {\n required: (value: unknown, message = 'This field is required'): string | null => {\n if (value === null || value === undefined || value === '') {\n return message\n }\n if (Array.isArray(value) && value.length === 0) {\n return message\n }\n return null\n },\n\n minLength: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length < min) {\n return message || `Must be at least ${min} characters`\n }\n return null\n },\n\n maxLength: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (value.length > max) {\n return message || `Must be at most ${max} characters`\n }\n return null\n },\n\n min: (value: unknown, min: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value < min) {\n return message || `Must be at least ${min}`\n }\n return null\n },\n\n max: (value: unknown, max: number, message?: string): string | null => {\n if (typeof value !== 'number') return null\n if (value > max) {\n return message || `Must be at most ${max}`\n }\n return null\n },\n\n pattern: (value: unknown, pattern: RegExp, message?: string): string | null => {\n if (typeof value !== 'string') return null\n if (!pattern.test(value)) {\n return message || 'Invalid format'\n }\n return null\n },\n\n email: (value: unknown, message = 'Invalid email address'): string | null => {\n if (typeof value !== 'string' || !value) return null\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n if (!emailRegex.test(value)) {\n return message\n }\n return null\n },\n}\n\n/**\n * Form state management composable with validation.\n *\n * @param initialValues - Initial form values\n * @param rules - Validation rules for each field\n *\n * @example\n * ```typescript\n * const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(\n * { email: '', password: '' },\n * {\n * email: { required: true, email: true },\n * password: { required: true, minLength: 8 },\n * }\n * )\n *\n * // In template\n * <BaseInput v-bind=\"getFieldProps('email')\" label=\"Email\" />\n * <BaseInput v-bind=\"getFieldProps('password')\" type=\"password\" label=\"Password\" />\n * <BaseButton @click=\"handleSubmit(onSubmit)\" :disabled=\"!isValid\">Submit</BaseButton>\n * ```\n */\nexport function useForm<T extends Record<string, unknown>>(\n initialValues: T,\n rules: Partial<Record<keyof T, FieldRules>> = {}\n): UseFormReturn<T> {\n // Deep copy initial values so nested objects are not shared\n const _initialValues = structuredClone(initialValues)\n\n // Reactive form data\n const data = reactive(structuredClone(initialValues)) as T\n\n // Field state - use simple Record types for better TS compatibility\n const errors = reactive<Record<string, string | null>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = null\n return acc\n }, {} as Record<string, string | null>)\n )\n\n const touched = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const dirty = reactive<Record<string, boolean>>(\n Object.keys(initialValues).reduce((acc, key) => {\n acc[key] = false\n return acc\n }, {} as Record<string, boolean>)\n )\n\n const isSubmitting = ref(false)\n\n // Watch data changes to track dirty state\n watch(\n () => ({ ...data }),\n (newData) => {\n for (const key of Object.keys(newData)) {\n dirty[key] = newData[key as keyof T] !== _initialValues[key as keyof T]\n }\n },\n { deep: true }\n )\n\n // Validate a single field\n function validateField(field: string): boolean {\n const value = data[field as keyof T]\n const fieldRules = rules[field as keyof T]\n\n if (!fieldRules) {\n errors[field] = null\n return true\n }\n\n // Check required\n if (fieldRules.required) {\n const message = typeof fieldRules.required === 'string' ? fieldRules.required : undefined\n const error = validators.required(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Skip other validations if empty and not required\n if (value === null || value === undefined || value === '') {\n errors[field] = null\n return true\n }\n\n // Check minLength\n if (fieldRules.minLength !== undefined) {\n const config = typeof fieldRules.minLength === 'number'\n ? { value: fieldRules.minLength, message: undefined }\n : fieldRules.minLength\n const error = validators.minLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check maxLength\n if (fieldRules.maxLength !== undefined) {\n const config = typeof fieldRules.maxLength === 'number'\n ? { value: fieldRules.maxLength, message: undefined }\n : fieldRules.maxLength\n const error = validators.maxLength(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check min\n if (fieldRules.min !== undefined) {\n const config = typeof fieldRules.min === 'number'\n ? { value: fieldRules.min, message: undefined }\n : fieldRules.min\n const error = validators.min(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check max\n if (fieldRules.max !== undefined) {\n const config = typeof fieldRules.max === 'number'\n ? { value: fieldRules.max, message: undefined }\n : fieldRules.max\n const error = validators.max(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check pattern\n if (fieldRules.pattern !== undefined) {\n const config = fieldRules.pattern instanceof RegExp\n ? { value: fieldRules.pattern, message: undefined }\n : fieldRules.pattern\n const error = validators.pattern(value, config.value, config.message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check email\n if (fieldRules.email) {\n const message = typeof fieldRules.email === 'string' ? fieldRules.email : undefined\n const error = validators.email(value, message)\n if (error) {\n errors[field] = error\n return false\n }\n }\n\n // Check custom validators\n if (fieldRules.custom) {\n const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom]\n for (const rule of customRules) {\n const error = rule(value, data as Record<string, unknown>)\n if (error) {\n errors[field] = error\n return false\n }\n }\n }\n\n errors[field] = null\n return true\n }\n\n // Validate all fields\n function validate(): boolean {\n let isAllValid = true\n for (const field of Object.keys(data)) {\n if (!validateField(field)) {\n isAllValid = false\n }\n }\n return isAllValid\n }\n\n // Computed overall validity\n const isValid = computed(() => {\n return Object.values(errors).every(error => error === null)\n })\n\n // Computed overall dirty state\n const isDirty = computed(() => {\n return Object.values(dirty).some(d => d)\n })\n\n // Set field value\n function setFieldValue<K extends keyof T>(field: K, value: T[K]): void {\n ;(data as Record<string, unknown>)[field as string] = value\n if (touched[field as string]) {\n validateField(field as string)\n }\n }\n\n // Set field error\n function setFieldError(field: string, error: string | null): void {\n errors[field] = error\n }\n\n // Set field touched\n function setFieldTouched(field: string, isTouched = true): void {\n touched[field] = isTouched\n if (isTouched) {\n validateField(field)\n }\n }\n\n // Reset form\n function reset(values?: Partial<T>): void {\n const resetValues = values ? { ..._initialValues, ...values } : _initialValues\n for (const key of Object.keys(data)) {\n ;(data as Record<string, unknown>)[key] = resetValues[key as keyof T]\n errors[key] = null\n touched[key] = false\n dirty[key] = false\n }\n }\n\n // Handle submit\n function handleSubmit(onSubmit: (data: T) => Promise<void> | void) {\n return async (e?: Event): Promise<void> => {\n e?.preventDefault()\n\n // Mark all fields as touched\n for (const field of Object.keys(data)) {\n touched[field] = true\n }\n\n if (!validate()) {\n return\n }\n\n isSubmitting.value = true\n try {\n await onSubmit(data)\n } finally {\n isSubmitting.value = false\n }\n }\n }\n\n // Get props for a field (for v-bind)\n function getFieldProps<K extends keyof T>(field: K) {\n const fieldStr = field as string\n return {\n modelValue: data[field],\n 'onUpdate:modelValue': (value: T[K]) => setFieldValue(field, value),\n onBlur: () => setFieldTouched(fieldStr),\n error: touched[fieldStr] ? errors[fieldStr] : null,\n }\n }\n\n return {\n data,\n errors,\n touched,\n dirty,\n isValid,\n isDirty,\n isSubmitting,\n setFieldValue,\n setFieldError,\n setFieldTouched,\n validateField,\n validate,\n reset,\n handleSubmit,\n getFieldProps,\n }\n}\n"],"names":[],"mappings":"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"}
@@ -6,6 +6,8 @@ const platformContext = ref({
6
6
  let allowedOrigins = /* @__PURE__ */ new Set();
7
7
  let allowAnyOrigin = false;
8
8
  let initialized = false;
9
+ let listenerCount = 0;
10
+ let currentHandler = null;
9
11
  function getOriginFromUrl(url) {
10
12
  try {
11
13
  const parsed = new URL(url);
@@ -120,11 +122,20 @@ function usePlatformContext(options = {}) {
120
122
  onMounted(() => {
121
123
  if (!initialized) {
122
124
  detectPlatform();
125
+ currentHandler = handlePlatformMessage;
123
126
  window.addEventListener("message", handlePlatformMessage);
124
127
  initialized = true;
125
128
  }
129
+ listenerCount++;
126
130
  });
127
131
  onUnmounted(() => {
132
+ listenerCount--;
133
+ if (listenerCount <= 0 && currentHandler) {
134
+ window.removeEventListener("message", currentHandler);
135
+ currentHandler = null;
136
+ initialized = false;
137
+ listenerCount = 0;
138
+ }
128
139
  });
129
140
  const isIntegrated = computed(() => platformContext.value.isIntegrated);
130
141
  const plugin = computed(() => platformContext.value.plugin);
@@ -1 +1 @@
1
- {"version":3,"file":"usePlatformContext.js","sources":["../../src/composables/usePlatformContext.ts"],"sourcesContent":["import { ref, computed, onMounted, onUnmounted } from 'vue'\nimport type { PlatformContext, PlatformContextOptions, PlatformEvent } from '../types'\n\nconst platformContext = ref<PlatformContext>({\n isIntegrated: false,\n theme: 'system',\n})\n\n// Track allowed origins for postMessage security\nlet allowedOrigins: Set<string> = new Set()\nlet allowAnyOrigin = false\nlet initialized = false\n\n/**\n * Derive origin from URL (protocol + host)\n */\nfunction getOriginFromUrl(url: string): string | null {\n try {\n const parsed = new URL(url)\n return parsed.origin\n } catch {\n return null\n }\n}\n\n/**\n * Check if an origin is allowed for postMessage communication\n */\nfunction isOriginAllowed(origin: string): boolean {\n // Development mode: allow any origin (must be explicitly enabled)\n if (allowAnyOrigin) {\n console.warn('[MLD SDK] postMessage origin validation disabled - only use in development')\n return true\n }\n\n // Same origin is always allowed\n if (origin === window.location.origin) {\n return true\n }\n\n // Check against allowed origins list\n return allowedOrigins.has(origin)\n}\n\n/**\n * Platform context composable for plugin integration with MLD Platform.\n *\n * Provides secure communication with the parent platform via postMessage.\n *\n * @param options - Configuration options\n * @param options.allowedOrigins - List of allowed origins for postMessage\n * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)\n *\n * @example\n * ```typescript\n * // Basic usage - derives origin from platform injection\n * const { isIntegrated, user, theme } = usePlatformContext()\n *\n * // With explicit allowed origins\n * const { isIntegrated } = usePlatformContext({\n * allowedOrigins: ['https://mld.example.com']\n * })\n *\n * // Development mode (UNSAFE)\n * const { isIntegrated } = usePlatformContext({\n * allowAnyOrigin: import.meta.env.DEV\n * })\n * ```\n */\nexport function usePlatformContext(options: PlatformContextOptions = {}) {\n function detectPlatform(): void {\n // Check if running under MLD Platform by looking for platform-injected global\n const platformData = (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_PLATFORM__\n\n if (platformData) {\n platformContext.value = {\n ...platformData,\n isIntegrated: true,\n }\n\n // Derive platform origin from injected data\n if (platformData.platformOrigin) {\n allowedOrigins.add(platformData.platformOrigin)\n } else if (platformData.platformApiUrl) {\n const origin = getOriginFromUrl(platformData.platformApiUrl)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n } else {\n // Check for platform indicator in URL or localStorage\n const urlParams = new URLSearchParams(window.location.search)\n const hasPluginParam = urlParams.has('mld-plugin')\n\n // Try to get platform origin from URL parameter\n const platformOrigin = urlParams.get('mld-origin')\n if (platformOrigin) {\n const origin = getOriginFromUrl(platformOrigin)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n\n platformContext.value = {\n isIntegrated: hasPluginParam,\n theme: (localStorage.getItem('mld-theme') as 'light' | 'dark' | 'system') || 'system',\n platformOrigin: platformOrigin || undefined,\n }\n }\n\n // Add user-provided allowed origins\n if (options.allowedOrigins) {\n for (const origin of options.allowedOrigins) {\n const normalized = getOriginFromUrl(origin) || origin\n allowedOrigins.add(normalized)\n }\n }\n\n // Set development mode flag\n if (options.allowAnyOrigin) {\n allowAnyOrigin = true\n }\n }\n\n function handlePlatformMessage(event: MessageEvent): void {\n // Only accept messages from parent window (platform)\n if (event.source !== window.parent) return\n\n // Validate origin for security\n if (!isOriginAllowed(event.origin)) {\n console.warn(`[MLD SDK] Rejected postMessage from untrusted origin: ${event.origin}`)\n return\n }\n\n try {\n const platformEvent = event.data as PlatformEvent\n if (!platformEvent.type?.startsWith('mld:')) return\n\n switch (platformEvent.type) {\n case 'mld:theme-changed':\n platformContext.value.theme = platformEvent.payload as 'light' | 'dark' | 'system'\n break\n case 'mld:user-changed':\n platformContext.value.user = platformEvent.payload as PlatformContext['user']\n break\n }\n } catch {\n // Ignore invalid messages\n }\n }\n\n /**\n * Send a message to the parent platform.\n * Uses validated target origin for security.\n */\n function sendToPlatform(event: PlatformEvent): void {\n if (!platformContext.value.isIntegrated || window.parent === window) {\n return\n }\n\n // Determine target origin\n let targetOrigin: string\n\n if (platformContext.value.platformOrigin) {\n // Use explicitly configured platform origin\n targetOrigin = platformContext.value.platformOrigin\n } else if (allowedOrigins.size > 0) {\n // Use first allowed origin (typically the platform)\n targetOrigin = allowedOrigins.values().next().value as string\n } else if (allowAnyOrigin) {\n // Development mode fallback\n targetOrigin = '*'\n console.warn('[MLD SDK] Using wildcard origin for postMessage - only use in development')\n } else {\n // Safety: if no origin is configured, log warning and don't send\n console.warn('[MLD SDK] Cannot send postMessage: no platform origin configured')\n return\n }\n\n window.parent.postMessage(event, targetOrigin)\n }\n\n /**\n * Request navigation to a path in the platform.\n */\n function navigate(path: string): void {\n sendToPlatform({\n type: 'mld:navigate',\n payload: path,\n })\n }\n\n /**\n * Show a notification in the platform.\n */\n function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {\n sendToPlatform({\n type: 'mld:notification',\n payload: { message, type },\n })\n }\n\n onMounted(() => {\n if (!initialized) {\n detectPlatform()\n window.addEventListener('message', handlePlatformMessage)\n initialized = true\n }\n })\n\n onUnmounted(() => {\n // Don't remove listener as other components may still need it\n })\n\n const isIntegrated = computed(() => platformContext.value.isIntegrated)\n const plugin = computed(() => platformContext.value.plugin)\n const user = computed(() => platformContext.value.user)\n const theme = computed(() => platformContext.value.theme)\n const features = computed(() => platformContext.value.features)\n\n return {\n context: platformContext,\n isIntegrated,\n plugin,\n user,\n theme,\n features,\n navigate,\n notify,\n sendToPlatform,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usePlatformContext.js","sources":["../../src/composables/usePlatformContext.ts"],"sourcesContent":["import { ref, computed, onMounted, onUnmounted } from 'vue'\nimport type { PlatformContext, PlatformContextOptions, PlatformEvent } from '../types'\n\nconst platformContext = ref<PlatformContext>({\n isIntegrated: false,\n theme: 'system',\n})\n\n// Track allowed origins for postMessage security\nlet allowedOrigins: Set<string> = new Set()\nlet allowAnyOrigin = false\nlet initialized = false\nlet listenerCount = 0\nlet currentHandler: ((event: MessageEvent) => void) | null = null\n\n/**\n * Derive origin from URL (protocol + host)\n */\nfunction getOriginFromUrl(url: string): string | null {\n try {\n const parsed = new URL(url)\n return parsed.origin\n } catch {\n return null\n }\n}\n\n/**\n * Check if an origin is allowed for postMessage communication\n */\nfunction isOriginAllowed(origin: string): boolean {\n // Development mode: allow any origin (must be explicitly enabled)\n if (allowAnyOrigin) {\n console.warn('[MLD SDK] postMessage origin validation disabled - only use in development')\n return true\n }\n\n // Same origin is always allowed\n if (origin === window.location.origin) {\n return true\n }\n\n // Check against allowed origins list\n return allowedOrigins.has(origin)\n}\n\n/**\n * Platform context composable for plugin integration with MLD Platform.\n *\n * Provides secure communication with the parent platform via postMessage.\n *\n * @param options - Configuration options\n * @param options.allowedOrigins - List of allowed origins for postMessage\n * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)\n *\n * @example\n * ```typescript\n * // Basic usage - derives origin from platform injection\n * const { isIntegrated, user, theme } = usePlatformContext()\n *\n * // With explicit allowed origins\n * const { isIntegrated } = usePlatformContext({\n * allowedOrigins: ['https://mld.example.com']\n * })\n *\n * // Development mode (UNSAFE)\n * const { isIntegrated } = usePlatformContext({\n * allowAnyOrigin: import.meta.env.DEV\n * })\n * ```\n */\nexport function usePlatformContext(options: PlatformContextOptions = {}) {\n function detectPlatform(): void {\n // Check if running under MLD Platform by looking for platform-injected global\n const platformData = (window as unknown as { __MLD_PLATFORM__?: PlatformContext }).__MLD_PLATFORM__\n\n if (platformData) {\n platformContext.value = {\n ...platformData,\n isIntegrated: true,\n }\n\n // Derive platform origin from injected data\n if (platformData.platformOrigin) {\n allowedOrigins.add(platformData.platformOrigin)\n } else if (platformData.platformApiUrl) {\n const origin = getOriginFromUrl(platformData.platformApiUrl)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n } else {\n // Check for platform indicator in URL or localStorage\n const urlParams = new URLSearchParams(window.location.search)\n const hasPluginParam = urlParams.has('mld-plugin')\n\n // Try to get platform origin from URL parameter\n const platformOrigin = urlParams.get('mld-origin')\n if (platformOrigin) {\n const origin = getOriginFromUrl(platformOrigin)\n if (origin) {\n allowedOrigins.add(origin)\n }\n }\n\n platformContext.value = {\n isIntegrated: hasPluginParam,\n theme: (localStorage.getItem('mld-theme') as 'light' | 'dark' | 'system') || 'system',\n platformOrigin: platformOrigin || undefined,\n }\n }\n\n // Add user-provided allowed origins\n if (options.allowedOrigins) {\n for (const origin of options.allowedOrigins) {\n const normalized = getOriginFromUrl(origin) || origin\n allowedOrigins.add(normalized)\n }\n }\n\n // Set development mode flag\n if (options.allowAnyOrigin) {\n allowAnyOrigin = true\n }\n }\n\n function handlePlatformMessage(event: MessageEvent): void {\n // Only accept messages from parent window (platform)\n if (event.source !== window.parent) return\n\n // Validate origin for security\n if (!isOriginAllowed(event.origin)) {\n console.warn(`[MLD SDK] Rejected postMessage from untrusted origin: ${event.origin}`)\n return\n }\n\n try {\n const platformEvent = event.data as PlatformEvent\n if (!platformEvent.type?.startsWith('mld:')) return\n\n switch (platformEvent.type) {\n case 'mld:theme-changed':\n platformContext.value.theme = platformEvent.payload as 'light' | 'dark' | 'system'\n break\n case 'mld:user-changed':\n platformContext.value.user = platformEvent.payload as PlatformContext['user']\n break\n }\n } catch {\n // Ignore invalid messages\n }\n }\n\n /**\n * Send a message to the parent platform.\n * Uses validated target origin for security.\n */\n function sendToPlatform(event: PlatformEvent): void {\n if (!platformContext.value.isIntegrated || window.parent === window) {\n return\n }\n\n // Determine target origin\n let targetOrigin: string\n\n if (platformContext.value.platformOrigin) {\n // Use explicitly configured platform origin\n targetOrigin = platformContext.value.platformOrigin\n } else if (allowedOrigins.size > 0) {\n // Use first allowed origin (typically the platform)\n targetOrigin = allowedOrigins.values().next().value as string\n } else if (allowAnyOrigin) {\n // Development mode fallback\n targetOrigin = '*'\n console.warn('[MLD SDK] Using wildcard origin for postMessage - only use in development')\n } else {\n // Safety: if no origin is configured, log warning and don't send\n console.warn('[MLD SDK] Cannot send postMessage: no platform origin configured')\n return\n }\n\n window.parent.postMessage(event, targetOrigin)\n }\n\n /**\n * Request navigation to a path in the platform.\n */\n function navigate(path: string): void {\n sendToPlatform({\n type: 'mld:navigate',\n payload: path,\n })\n }\n\n /**\n * Show a notification in the platform.\n */\n function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info'): void {\n sendToPlatform({\n type: 'mld:notification',\n payload: { message, type },\n })\n }\n\n onMounted(() => {\n if (!initialized) {\n detectPlatform()\n currentHandler = handlePlatformMessage\n window.addEventListener('message', handlePlatformMessage)\n initialized = true\n }\n listenerCount++\n })\n\n onUnmounted(() => {\n listenerCount--\n if (listenerCount <= 0 && currentHandler) {\n window.removeEventListener('message', currentHandler)\n currentHandler = null\n initialized = false\n listenerCount = 0\n }\n })\n\n const isIntegrated = computed(() => platformContext.value.isIntegrated)\n const plugin = computed(() => platformContext.value.plugin)\n const user = computed(() => platformContext.value.user)\n const theme = computed(() => platformContext.value.theme)\n const features = computed(() => platformContext.value.features)\n\n return {\n context: platformContext,\n isIntegrated,\n plugin,\n user,\n theme,\n features,\n navigate,\n notify,\n sendToPlatform,\n }\n}\n"],"names":[],"mappings":"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"}