@verbb/plugin-kit-core 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.0.5 - 2026-07-21
6
+
7
+ ### Fixed
8
+ - Importing string/handle utils (or anything else) from the package root no longer bundles `markdown-it` (~45 kB gzip) into a consumer. `markdown-it` is now constructed lazily and the package is published as `sideEffects: false`, so it only enters a bundle when `renderMarkdown` / `renderInlineMarkdown` (or `@verbb/plugin-kit-core/utils/markdown`) is actually used.
9
+
10
+ ### Added
11
+ - Granular subpath exports so consumers can cherry-pick exactly what they need: `./utils`, `./utils/string`, `./utils/handle`, `./utils/markdown`, and `./host`. The build now preserves the `src` module structure (`dist/utils/string.js`, `dist/host/index.js`, …) instead of one concatenated bundle.
12
+
13
+ ### Changed
14
+ - The package main entry is now `dist/index.js` (was `dist/plugin-kit-core.es.js`). Consumers importing from `@verbb/plugin-kit-core` via the `exports` map are unaffected; anything deep-linking the old bundle filename must switch to the package root or a subpath export.
15
+
16
+ ### Removed
17
+ - **Breaking:** removed the eager `export { md }` markdown-it instance (an exported pre-built instance is itself a module-level side effect that defeats tree-shaking). Use the new `getMarkdownIt()` accessor for direct markdown-it API access.
18
+
5
19
  ## 2.0.4 - 2026-07-21
6
20
 
7
21
  ### Changed
@@ -0,0 +1,35 @@
1
+ //#region src/host/hostBridge.ts
2
+ var hostBridge = {};
3
+ var setHostBridge = (bridge = {}) => {
4
+ hostBridge = {
5
+ ...hostBridge,
6
+ ...bridge
7
+ };
8
+ };
9
+ var getHostBridge = () => {
10
+ return hostBridge;
11
+ };
12
+ var requireHostBridgeMethod = (methodName) => {
13
+ const method = hostBridge[methodName];
14
+ if (!method) throw new Error(`Plugin Kit host bridge method "${String(methodName)}" is required but was not configured.`);
15
+ return method;
16
+ };
17
+ var hostRequest = async (method, action, config) => {
18
+ return requireHostBridgeMethod("request")(method, action, config);
19
+ };
20
+ var hostOpenElementSelector = (elementType, options) => {
21
+ requireHostBridgeMethod("openElementSelector")(elementType, options);
22
+ };
23
+ var hostFormatDate = (date) => {
24
+ return requireHostBridgeMethod("formatDate")(date);
25
+ };
26
+ var hostGetTimepickerOptions = () => {
27
+ return requireHostBridgeMethod("getTimepickerOptions")();
28
+ };
29
+ var hostGetLocale = () => {
30
+ return requireHostBridgeMethod("getLocale")();
31
+ };
32
+ //#endregion
33
+ export { getHostBridge, hostFormatDate, hostGetLocale, hostGetTimepickerOptions, hostOpenElementSelector, hostRequest, setHostBridge };
34
+
35
+ //# sourceMappingURL=hostBridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hostBridge.js","names":[],"sources":["../../src/host/hostBridge.ts"],"sourcesContent":["export type HostRequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\nexport type HostRequestConfig = {\n data?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type HostSelectedElement = {\n id?: number;\n siteId?: number;\n label?: string;\n url?: string;\n [key: string]: unknown;\n};\n\nexport type HostElementSelectorOptions = {\n storageKey: string;\n sources?: string[];\n criteria?: Record<string, unknown>;\n multiSelect?: boolean;\n limit?: number | null;\n defaultSiteId?: number;\n autoFocusSearchBox?: boolean;\n showSiteMenu?: boolean;\n onShow?: () => void;\n onSelect: (elements: HostSelectedElement[]) => void;\n closeOtherModals?: boolean;\n};\n\nexport type PluginKitHostBridge = {\n request: <T = unknown>(method: HostRequestMethod, action: string, config?: HostRequestConfig) => Promise<T>;\n openElementSelector: (elementType: string, options: HostElementSelectorOptions) => void;\n formatDate: (date: Date) => string;\n getTimepickerOptions: () => Record<string, unknown>;\n getLocale: () => string;\n};\n\nlet hostBridge: Partial<PluginKitHostBridge> = {};\n\nexport const setHostBridge = (bridge: Partial<PluginKitHostBridge> = {}): void => {\n hostBridge = {\n ...hostBridge,\n ...bridge,\n };\n};\n\nexport const getHostBridge = (): Partial<PluginKitHostBridge> => {\n return hostBridge;\n};\n\nconst requireHostBridgeMethod = <K extends keyof PluginKitHostBridge>(\n methodName: K,\n): NonNullable<PluginKitHostBridge[K]> => {\n const method = hostBridge[methodName];\n\n if (!method) {\n throw new Error(`Plugin Kit host bridge method \"${String(methodName)}\" is required but was not configured.`);\n }\n\n return method as NonNullable<PluginKitHostBridge[K]>;\n};\n\nexport const hostRequest = async <T = unknown>(\n method: HostRequestMethod,\n action: string,\n config?: HostRequestConfig,\n): Promise<T> => {\n const request = requireHostBridgeMethod('request');\n return request(method, action, config);\n};\n\nexport const hostOpenElementSelector = (elementType: string, options: HostElementSelectorOptions): void => {\n const openElementSelector = requireHostBridgeMethod('openElementSelector');\n openElementSelector(elementType, options);\n};\n\nexport const hostFormatDate = (date: Date): string => {\n const formatDate = requireHostBridgeMethod('formatDate');\n return formatDate(date);\n};\n\nexport const hostGetTimepickerOptions = (): Record<string, unknown> => {\n const getTimepickerOptions = requireHostBridgeMethod('getTimepickerOptions');\n return getTimepickerOptions();\n};\n\nexport const hostGetLocale = (): string => {\n const getLocale = requireHostBridgeMethod('getLocale');\n return getLocale();\n};\n"],"mappings":";AAqCA,IAAI,aAA2C,EAAE;AAEjD,IAAa,iBAAiB,SAAuC,EAAE,KAAW;AAC9E,cAAa;EACT,GAAG;EACH,GAAG;EACN;;AAGL,IAAa,sBAAoD;AAC7D,QAAO;;AAGX,IAAM,2BACF,eACsC;CACtC,MAAM,SAAS,WAAW;AAE1B,KAAI,CAAC,OACD,OAAM,IAAI,MAAM,kCAAkC,OAAO,WAAW,CAAC,uCAAuC;AAGhH,QAAO;;AAGX,IAAa,cAAc,OACvB,QACA,QACA,WACa;AAEb,QADgB,wBAAwB,UACjC,CAAQ,QAAQ,QAAQ,OAAO;;AAG1C,IAAa,2BAA2B,aAAqB,YAA8C;AAC3E,yBAAwB,sBACpD,CAAoB,aAAa,QAAQ;;AAG7C,IAAa,kBAAkB,SAAuB;AAElD,QADmB,wBAAwB,aACpC,CAAW,KAAK;;AAG3B,IAAa,iCAA0D;AAEnE,QAD6B,wBAAwB,uBAC9C,EAAsB;;AAGjC,IAAa,sBAA8B;AAEvC,QADkB,wBAAwB,YACnC,EAAW"}
@@ -0,0 +1,52 @@
1
+ //#region src/host/portal.ts
2
+ var defaultPortalClassName;
3
+ var defaultPortalContainer;
4
+ var defaultShadowRootSelectors = ["[data-pk-shadow-root]"];
5
+ var setPortalClassName = (className) => {
6
+ defaultPortalClassName = className.trim() || void 0;
7
+ };
8
+ var getPortalClassName = (className) => {
9
+ return (className ?? defaultPortalClassName)?.trim() || void 0;
10
+ };
11
+ var resolvePortalContainer = (container) => {
12
+ if (!container) return;
13
+ if (typeof container === "object" && "current" in container) return container.current ?? void 0;
14
+ return container ?? void 0;
15
+ };
16
+ var setPortalContainer = (container) => {
17
+ defaultPortalContainer = resolvePortalContainer(container);
18
+ };
19
+ var getPortalContainer = (container) => {
20
+ return resolvePortalContainer(container) ?? defaultPortalContainer;
21
+ };
22
+ var setShadowRootSelectors = (selectors) => {
23
+ const normalizedSelectors = (selectors || []).map((selector) => selector.trim()).filter(Boolean);
24
+ defaultShadowRootSelectors = normalizedSelectors.length > 0 ? normalizedSelectors : ["[data-pk-shadow-root]"];
25
+ };
26
+ var getShadowRootSelectors = () => {
27
+ return defaultShadowRootSelectors;
28
+ };
29
+ /**
30
+ * Resolve where floating content should be portaled.
31
+ * Falls back to document.body when no container is configured.
32
+ */
33
+ var getPortalMountNode = (container) => {
34
+ const resolved = getPortalContainer(container);
35
+ if (resolved instanceof HTMLElement) return resolved;
36
+ if (resolved instanceof ShadowRoot) {
37
+ const target = getShadowRootSelectors().map((selector) => resolved.querySelector(selector)).find(Boolean) ?? (resolved.host instanceof HTMLElement ? resolved.host : void 0);
38
+ if (target) return target;
39
+ }
40
+ return document.body;
41
+ };
42
+ var getPortalTargetForAppend = () => {
43
+ const container = getPortalContainer();
44
+ if (!container) return;
45
+ if (container instanceof HTMLElement) return container;
46
+ const shadowRoot = container;
47
+ return getShadowRootSelectors().map((selector) => shadowRoot.querySelector(selector)).find(Boolean) ?? (shadowRoot.host instanceof HTMLElement ? shadowRoot.host : void 0);
48
+ };
49
+ //#endregion
50
+ export { getPortalClassName, getPortalContainer, getPortalMountNode, getPortalTargetForAppend, getShadowRootSelectors, setPortalClassName, setPortalContainer, setShadowRootSelectors };
51
+
52
+ //# sourceMappingURL=portal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal.js","names":[],"sources":["../../src/host/portal.ts"],"sourcesContent":["export type PortalContainer = HTMLElement | ShadowRoot | null;\n\nexport type PortalContainerRef = {\n current: PortalContainer;\n};\n\nexport type PortalContainerInput = PortalContainer | PortalContainerRef;\n\nlet defaultPortalClassName: string | undefined;\nlet defaultPortalContainer: PortalContainer | undefined;\nlet defaultShadowRootSelectors: string[] = ['[data-pk-shadow-root]'];\n\nexport const setPortalClassName = (className: string): void => {\n defaultPortalClassName = className.trim() || undefined;\n};\n\nexport const getPortalClassName = (className?: string): string | undefined => {\n return (className ?? defaultPortalClassName)?.trim() || undefined;\n};\n\nconst resolvePortalContainer = (container?: PortalContainerInput): PortalContainer | undefined => {\n if (!container) {\n return undefined;\n }\n\n if (typeof container === 'object' && 'current' in container) {\n return container.current ?? undefined;\n }\n\n return container ?? undefined;\n};\n\nexport const setPortalContainer = (container: PortalContainerInput): void => {\n defaultPortalContainer = resolvePortalContainer(container);\n};\n\nexport const getPortalContainer = (container?: PortalContainerInput): PortalContainer | undefined => {\n return resolvePortalContainer(container) ?? defaultPortalContainer;\n};\n\nexport const setShadowRootSelectors = (selectors: string[]): void => {\n const normalizedSelectors = (selectors || [])\n .map((selector) => selector.trim())\n .filter(Boolean);\n\n defaultShadowRootSelectors = normalizedSelectors.length > 0\n ? normalizedSelectors\n : ['[data-pk-shadow-root]'];\n};\n\nexport const getShadowRootSelectors = (): string[] => {\n return defaultShadowRootSelectors;\n};\n\n/**\n * Resolve where floating content should be portaled.\n * Falls back to document.body when no container is configured.\n */\nexport const getPortalMountNode = (container?: PortalContainerInput): HTMLElement => {\n const resolved = getPortalContainer(container);\n\n if (resolved instanceof HTMLElement) {\n return resolved;\n }\n\n if (resolved instanceof ShadowRoot) {\n const target = getShadowRootSelectors()\n .map((selector) => resolved.querySelector<HTMLElement>(selector))\n .find(Boolean)\n ?? (resolved.host instanceof HTMLElement ? resolved.host : undefined);\n\n if (target) {\n return target;\n }\n }\n\n return document.body;\n};\n\nexport const getPortalTargetForAppend = (): HTMLElement | undefined => {\n const container = getPortalContainer();\n if (!container) {\n return undefined;\n }\n\n if (container instanceof HTMLElement) {\n return container;\n }\n\n const shadowRoot = container;\n return getShadowRootSelectors()\n .map((selector) => shadowRoot.querySelector<HTMLElement>(selector))\n .find(Boolean)\n ?? (shadowRoot.host instanceof HTMLElement ? shadowRoot.host : undefined);\n};\n"],"mappings":";AAQA,IAAI;AACJ,IAAI;AACJ,IAAI,6BAAuC,CAAC,wBAAwB;AAEpE,IAAa,sBAAsB,cAA4B;AAC3D,0BAAyB,UAAU,MAAM,IAAI,KAAA;;AAGjD,IAAa,sBAAsB,cAA2C;AAC1E,SAAQ,aAAa,yBAAyB,MAAM,IAAI,KAAA;;AAG5D,IAAM,0BAA0B,cAAkE;AAC9F,KAAI,CAAC,UACD;AAGJ,KAAI,OAAO,cAAc,YAAY,aAAa,UAC9C,QAAO,UAAU,WAAW,KAAA;AAGhC,QAAO,aAAa,KAAA;;AAGxB,IAAa,sBAAsB,cAA0C;AACzE,0BAAyB,uBAAuB,UAAU;;AAG9D,IAAa,sBAAsB,cAAkE;AACjG,QAAO,uBAAuB,UAAU,IAAI;;AAGhD,IAAa,0BAA0B,cAA8B;CACjE,MAAM,uBAAuB,aAAa,EAAE,EACvC,KAAK,aAAa,SAAS,MAAM,CAAC,CAClC,OAAO,QAAQ;AAEpB,8BAA6B,oBAAoB,SAAS,IACpD,sBACA,CAAC,wBAAwB;;AAGnC,IAAa,+BAAyC;AAClD,QAAO;;;;;;AAOX,IAAa,sBAAsB,cAAkD;CACjF,MAAM,WAAW,mBAAmB,UAAU;AAE9C,KAAI,oBAAoB,YACpB,QAAO;AAGX,KAAI,oBAAoB,YAAY;EAChC,MAAM,SAAS,wBAAwB,CAClC,KAAK,aAAa,SAAS,cAA2B,SAAS,CAAC,CAChE,KAAK,QAAQ,KACV,SAAS,gBAAgB,cAAc,SAAS,OAAO,KAAA;AAE/D,MAAI,OACA,QAAO;;AAIf,QAAO,SAAS;;AAGpB,IAAa,iCAA0D;CACnE,MAAM,YAAY,oBAAoB;AACtC,KAAI,CAAC,UACD;AAGJ,KAAI,qBAAqB,YACrB,QAAO;CAGX,MAAM,aAAa;AACnB,QAAO,wBAAwB,CAC1B,KAAK,aAAa,WAAW,cAA2B,SAAS,CAAC,CAClE,KAAK,QAAQ,KACV,WAAW,gBAAgB,cAAc,WAAW,OAAO,KAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { getPortalClassName, getPortalContainer, getPortalMountNode, getPortalTargetForAppend, getShadowRootSelectors, setPortalClassName, setPortalContainer, setShadowRootSelectors } from "./host/portal.js";
2
+ import { getHostBridge, hostFormatDate, hostGetLocale, hostGetTimepickerOptions, hostOpenElementSelector, hostRequest, setHostBridge } from "./host/hostBridge.js";
3
+ import { clone, createItem, deleteItem, duplicateItem, findItemById, findRecursive, generateId, getExistingItems, getNewItems, moveItem, normalizeCollection, updateItem } from "./utils/collections.js";
4
+ import { getErrorMessage } from "./utils/forms.js";
5
+ import { findUniqueHandle, generateHandle } from "./utils/string.js";
6
+ import { buildUniqueHandleFromSource, getDynamicReservedHandles } from "./utils/handle.js";
7
+ import { getMarkdownIt, renderInlineMarkdown, renderMarkdown } from "./utils/markdown.js";
8
+ import { takeAtLeast } from "./utils/promises.js";
9
+ import { getQueryParam, setQueryParam } from "./utils/query.js";
10
+ export { buildUniqueHandleFromSource, clone, createItem, deleteItem, duplicateItem, findItemById, findRecursive, findUniqueHandle, generateHandle, generateId, getDynamicReservedHandles, getErrorMessage, getExistingItems, getHostBridge, getMarkdownIt, getNewItems, getPortalClassName, getPortalContainer, getPortalMountNode, getPortalTargetForAppend, getQueryParam, getShadowRootSelectors, hostFormatDate, hostGetLocale, hostGetTimepickerOptions, hostOpenElementSelector, hostRequest, moveItem, normalizeCollection, renderInlineMarkdown, renderMarkdown, setHostBridge, setPortalClassName, setPortalContainer, setQueryParam, setShadowRootSelectors, takeAtLeast, updateItem };
@@ -0,0 +1,155 @@
1
+ //#region src/utils/collections.ts
2
+ /**
3
+ * Collections utility for managing client-side collections of models
4
+ */
5
+ /**
6
+ * Generates a unique client-side ID string
7
+ * @param {string} [prefix='client'] - Optional prefix for the generated ID
8
+ * @returns {string} A unique ID string in the format: prefix_timestamp_randomString
9
+ */
10
+ var generateId = (prefix = "client") => {
11
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
12
+ };
13
+ /**
14
+ * Normalize a collection by adding _id to each item that doesn't have one
15
+ * @param {Array} collection - Array of items
16
+ * @returns {Array} - Normalized collection with _id on each item
17
+ */
18
+ var normalizeCollection = (collection = []) => {
19
+ return collection.map((item) => {
20
+ if (item._id) return item;
21
+ return {
22
+ ...item,
23
+ _id: generateId()
24
+ };
25
+ });
26
+ };
27
+ /**
28
+ * Create a new collection item
29
+ * @param {Object} itemData - Initial data for the new item
30
+ * @returns {Object} - The new item
31
+ */
32
+ var createItem = (itemData = {}) => {
33
+ return {
34
+ ...itemData,
35
+ _id: generateId()
36
+ };
37
+ };
38
+ /**
39
+ * Duplicate an existing item in a collection
40
+ * @param {Array} collection - Current collection
41
+ * @param {Object} item - Item to duplicate
42
+ * @param {Function} transformCallback - Optional callback to transform the duplicated item
43
+ * @returns {Object} - Updated collection with duplicated item
44
+ */
45
+ var duplicateItem = (collection = [], item, transformCallback = null) => {
46
+ const duplicatedItem = {
47
+ ...item,
48
+ _id: generateId()
49
+ };
50
+ const finalItem = transformCallback ? transformCallback(duplicatedItem) : duplicatedItem;
51
+ return [...collection, finalItem];
52
+ };
53
+ /**
54
+ * Delete a item from a collection
55
+ * @param {Array} collection - Current collection
56
+ * @param {Object} item - Item to delete
57
+ * @returns {Object} - Updated collection without the deleted item
58
+ */
59
+ var deleteItem = (collection = [], itemToDelete) => {
60
+ return collection.filter((item) => {
61
+ return item._id !== itemToDelete._id;
62
+ });
63
+ };
64
+ /**
65
+ * Update an item in a collection
66
+ * @param {Array} collection - Current collection
67
+ * @param {Object} item - Item to update
68
+ * @param {Object} updates - Updates to apply to the item
69
+ * @returns {Object} - Updated collection with updated item
70
+ */
71
+ var updateItem = (collection = [], itemToUpdate, updates) => {
72
+ return collection.map((item) => {
73
+ if (item._id === itemToUpdate._id) return {
74
+ ...item,
75
+ ...updates,
76
+ _id: item._id
77
+ };
78
+ return item;
79
+ });
80
+ };
81
+ /**
82
+ * Move an item to a new position in a collection
83
+ * @param {Array} collection - Current collection
84
+ * @param {Object} fromItem - Item to move
85
+ * @param {Object} toItem - Item to move to (insert before this item)
86
+ * @returns {Array} - Updated collection with moved item
87
+ */
88
+ var moveItem = (collection = [], fromItem, toItem) => {
89
+ if (fromItem._id === toItem._id) return collection;
90
+ const fromIndex = collection.findIndex((item) => {
91
+ return item._id === fromItem._id;
92
+ });
93
+ const toIndex = collection.findIndex((item) => {
94
+ return item._id === toItem._id;
95
+ });
96
+ if (fromIndex === -1 || toIndex === -1) return collection;
97
+ const newCollection = [...collection];
98
+ const [movedItem] = newCollection.splice(fromIndex, 1);
99
+ newCollection.splice(toIndex, 0, movedItem);
100
+ return newCollection;
101
+ };
102
+ /**
103
+ * Find an item in a collection by _id
104
+ * @param {Array} collection - Collection to search
105
+ * @param {string} _id - Client-side ID to find
106
+ * @returns {Object|null} - Found model or null
107
+ */
108
+ var findItemById = (collection = [], _id) => {
109
+ return collection.find((item) => {
110
+ return item._id === _id;
111
+ }) || null;
112
+ };
113
+ /**
114
+ * Get all items that have server-side IDs (existing items)
115
+ * @param {Array} collection - Collection to filter
116
+ * @returns {Array} - Items with server-side IDs
117
+ */
118
+ var getExistingItems = (collection = []) => {
119
+ return collection.filter((item) => {
120
+ return item.id;
121
+ });
122
+ };
123
+ /**
124
+ * Get all items that don't have server-side IDs (new items)
125
+ * @param {Array} collection - Collection to filter
126
+ * @returns {Array} - Items without server-side IDs
127
+ */
128
+ var getNewItems = (collection = []) => {
129
+ return collection.filter((item) => {
130
+ return !item.id;
131
+ });
132
+ };
133
+ var findRecursive = (items, predicate, optionsKey = "options") => {
134
+ for (const item of items) {
135
+ if (item[optionsKey] && Array.isArray(item[optionsKey])) {
136
+ const result = findRecursive(item[optionsKey], predicate, optionsKey);
137
+ if (result) return result;
138
+ }
139
+ if (predicate(item)) return item;
140
+ }
141
+ return null;
142
+ };
143
+ /**
144
+ * Creates a deep clone of a value using JSON serialization
145
+ * @param {any} value - The value to clone
146
+ * @returns {any} A deep clone of the input value, or undefined if input is undefined
147
+ */
148
+ var clone = function(value) {
149
+ if (value === void 0) return;
150
+ return JSON.parse(JSON.stringify(value));
151
+ };
152
+ //#endregion
153
+ export { clone, createItem, deleteItem, duplicateItem, findItemById, findRecursive, generateId, getExistingItems, getNewItems, moveItem, normalizeCollection, updateItem };
154
+
155
+ //# sourceMappingURL=collections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collections.js","names":[],"sources":["../../src/utils/collections.ts"],"sourcesContent":["/**\n * Collections utility for managing client-side collections of models\n */\n\n/**\n * Generates a unique client-side ID string\n * @param {string} [prefix='client'] - Optional prefix for the generated ID\n * @returns {string} A unique ID string in the format: prefix_timestamp_randomString\n */\nexport const generateId = (prefix = 'client'): string => {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n};\n\n/**\n * Normalize a collection by adding _id to each item that doesn't have one\n * @param {Array} collection - Array of items\n * @returns {Array} - Normalized collection with _id on each item\n */\nexport const normalizeCollection = (collection: any[] = []): any[] => {\n return collection.map((item) => {\n // If item already has _id, keep it (for client-side created items)\n if (item._id) {\n return item;\n }\n\n // Generate a client-side internal ID for server-side items\n return {\n ...item,\n _id: generateId(),\n };\n });\n};\n\n/**\n * Create a new collection item\n * @param {Object} itemData - Initial data for the new item\n * @returns {Object} - The new item\n */\nexport const createItem = (itemData: any = {}): any => {\n return {\n ...itemData,\n _id: generateId(),\n };\n};\n\n/**\n * Duplicate an existing item in a collection\n * @param {Array} collection - Current collection\n * @param {Object} item - Item to duplicate\n * @param {Function} transformCallback - Optional callback to transform the duplicated item\n * @returns {Object} - Updated collection with duplicated item\n */\nexport const duplicateItem = (collection: any[] = [], item: any, transformCallback: ((item: any) => any) | null = null): any[] => {\n // Create base duplicated item\n const duplicatedItem = {\n ...item,\n // Generate new client-side ID\n _id: generateId(),\n };\n\n // Apply custom transformation if provided\n const finalItem = transformCallback ? transformCallback(duplicatedItem) : duplicatedItem;\n\n return [...collection, finalItem];\n};\n\n/**\n * Delete a item from a collection\n * @param {Array} collection - Current collection\n * @param {Object} item - Item to delete\n * @returns {Object} - Updated collection without the deleted item\n */\nexport const deleteItem = (collection: any[] = [], itemToDelete: any): any[] => {\n return collection.filter((item) => { return item._id !== itemToDelete._id; });\n};\n\n/**\n * Update an item in a collection\n * @param {Array} collection - Current collection\n * @param {Object} item - Item to update\n * @param {Object} updates - Updates to apply to the item\n * @returns {Object} - Updated collection with updated item\n */\nexport const updateItem = (collection: any[] = [], itemToUpdate: any, updates: any): any[] => {\n return collection.map((item) => {\n if (item._id === itemToUpdate._id) {\n return {\n ...item,\n ...updates,\n // Preserve the _id\n _id: item._id,\n };\n }\n return item;\n });\n};\n\n/**\n * Move an item to a new position in a collection\n * @param {Array} collection - Current collection\n * @param {Object} fromItem - Item to move\n * @param {Object} toItem - Item to move to (insert before this item)\n * @returns {Array} - Updated collection with moved item\n */\nexport const moveItem = (collection: any[] = [], fromItem: any, toItem: any): any[] => {\n if (fromItem._id === toItem._id) {\n return collection;\n }\n\n const fromIndex = collection.findIndex((item) => { return item._id === fromItem._id; });\n const toIndex = collection.findIndex((item) => { return item._id === toItem._id; });\n\n if (fromIndex === -1 || toIndex === -1) {\n return collection;\n }\n\n const newCollection = [...collection];\n const [movedItem] = newCollection.splice(fromIndex, 1);\n newCollection.splice(toIndex, 0, movedItem);\n\n return newCollection;\n};\n\n/**\n * Find an item in a collection by _id\n * @param {Array} collection - Collection to search\n * @param {string} _id - Client-side ID to find\n * @returns {Object|null} - Found model or null\n */\nexport const findItemById = (collection: any[] = [], _id: string): any | null => {\n return collection.find((item) => { return item._id === _id; }) || null;\n};\n\n/**\n * Get all items that have server-side IDs (existing items)\n * @param {Array} collection - Collection to filter\n * @returns {Array} - Items with server-side IDs\n */\nexport const getExistingItems = (collection: any[] = []): any[] => {\n return collection.filter((item) => { return item.id; });\n};\n\n/**\n * Get all items that don't have server-side IDs (new items)\n * @param {Array} collection - Collection to filter\n * @returns {Array} - Items without server-side IDs\n */\nexport const getNewItems = (collection: any[] = []): any[] => {\n return collection.filter((item) => { return !item.id; });\n};\n\nexport const findRecursive = <T = any>(items: T[], predicate: (item: T) => boolean, optionsKey = 'options'): T | null => {\n for (const item of items) {\n // If this item has nested options, search recursively\n if ((item as any)[optionsKey] && Array.isArray((item as any)[optionsKey])) {\n const result = findRecursive((item as any)[optionsKey], predicate, optionsKey);\n\n if (result) {\n return result;\n }\n }\n\n // Check if this item matches the predicate\n if (predicate(item)) {\n return item;\n }\n }\n\n return null;\n};\n\n/**\n * Creates a deep clone of a value using JSON serialization\n * @param {any} value - The value to clone\n * @returns {any} A deep clone of the input value, or undefined if input is undefined\n */\nexport const clone = function <T>(value: T): T | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n return JSON.parse(JSON.stringify(value));\n};\n"],"mappings":";;;;;;;;;AASA,IAAa,cAAc,SAAS,aAAqB;AACrD,QAAO,GAAG,OAAO,GAAG,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,OAAO,GAAG,EAAE;;;;;;;AAQ7E,IAAa,uBAAuB,aAAoB,EAAE,KAAY;AAClE,QAAO,WAAW,KAAK,SAAS;AAE5B,MAAI,KAAK,IACL,QAAO;AAIX,SAAO;GACH,GAAG;GACH,KAAK,YAAY;GACpB;GACH;;;;;;;AAQN,IAAa,cAAc,WAAgB,EAAE,KAAU;AACnD,QAAO;EACH,GAAG;EACH,KAAK,YAAY;EACpB;;;;;;;;;AAUL,IAAa,iBAAiB,aAAoB,EAAE,EAAE,MAAW,oBAAiD,SAAgB;CAE9H,MAAM,iBAAiB;EACnB,GAAG;EAEH,KAAK,YAAY;EACpB;CAGD,MAAM,YAAY,oBAAoB,kBAAkB,eAAe,GAAG;AAE1E,QAAO,CAAC,GAAG,YAAY,UAAU;;;;;;;;AASrC,IAAa,cAAc,aAAoB,EAAE,EAAE,iBAA6B;AAC5E,QAAO,WAAW,QAAQ,SAAS;AAAE,SAAO,KAAK,QAAQ,aAAa;GAAO;;;;;;;;;AAUjF,IAAa,cAAc,aAAoB,EAAE,EAAE,cAAmB,YAAwB;AAC1F,QAAO,WAAW,KAAK,SAAS;AAC5B,MAAI,KAAK,QAAQ,aAAa,IAC1B,QAAO;GACH,GAAG;GACH,GAAG;GAEH,KAAK,KAAK;GACb;AAEL,SAAO;GACT;;;;;;;;;AAUN,IAAa,YAAY,aAAoB,EAAE,EAAE,UAAe,WAAuB;AACnF,KAAI,SAAS,QAAQ,OAAO,IACxB,QAAO;CAGX,MAAM,YAAY,WAAW,WAAW,SAAS;AAAE,SAAO,KAAK,QAAQ,SAAS;GAAO;CACvF,MAAM,UAAU,WAAW,WAAW,SAAS;AAAE,SAAO,KAAK,QAAQ,OAAO;GAAO;AAEnF,KAAI,cAAc,MAAM,YAAY,GAChC,QAAO;CAGX,MAAM,gBAAgB,CAAC,GAAG,WAAW;CACrC,MAAM,CAAC,aAAa,cAAc,OAAO,WAAW,EAAE;AACtD,eAAc,OAAO,SAAS,GAAG,UAAU;AAE3C,QAAO;;;;;;;;AASX,IAAa,gBAAgB,aAAoB,EAAE,EAAE,QAA4B;AAC7E,QAAO,WAAW,MAAM,SAAS;AAAE,SAAO,KAAK,QAAQ;GAAO,IAAI;;;;;;;AAQtE,IAAa,oBAAoB,aAAoB,EAAE,KAAY;AAC/D,QAAO,WAAW,QAAQ,SAAS;AAAE,SAAO,KAAK;GAAM;;;;;;;AAQ3D,IAAa,eAAe,aAAoB,EAAE,KAAY;AAC1D,QAAO,WAAW,QAAQ,SAAS;AAAE,SAAO,CAAC,KAAK;GAAM;;AAG5D,IAAa,iBAA0B,OAAY,WAAiC,aAAa,cAAwB;AACrH,MAAK,MAAM,QAAQ,OAAO;AAEtB,MAAK,KAAa,eAAe,MAAM,QAAS,KAAa,YAAY,EAAE;GACvE,MAAM,SAAS,cAAe,KAAa,aAAa,WAAW,WAAW;AAE9E,OAAI,OACA,QAAO;;AAKf,MAAI,UAAU,KAAK,CACf,QAAO;;AAIf,QAAO;;;;;;;AAQX,IAAa,QAAQ,SAAa,OAAyB;AACvD,KAAI,UAAU,KAAA,EACV;AAGJ,QAAO,KAAK,MAAM,KAAK,UAAU,MAAM,CAAC"}
@@ -0,0 +1,46 @@
1
+ //#region src/utils/forms.ts
2
+ var nl2br = (str) => {
3
+ return str.replace(/\n/g, "<br>");
4
+ };
5
+ var getErrorHeading = (error) => {
6
+ if (error.response?.statusText) return error.response.statusText;
7
+ if (error.message?.includes("Network Error")) return "Network Error";
8
+ if (error.message?.includes("timeout")) return "Request Timeout";
9
+ return "An error has occurred";
10
+ };
11
+ var getErrorText = (error) => {
12
+ if (error.response?.data?.message) return error.response.data.message;
13
+ if (error.response?.data?.error) return error.response.data.error;
14
+ if (error.message) return error.message;
15
+ return String(error);
16
+ };
17
+ var getErrorTrace = (error, maxTraceLines = 5) => {
18
+ const traces = [];
19
+ const file1 = error.response?.data?.file;
20
+ const line1 = error.response?.data?.line;
21
+ if (file1 && line1) traces.push(`${file1}:${line1}`);
22
+ const traceArray = error.response?.data?.trace || [];
23
+ for (let i = 0; i < Math.min(maxTraceLines, traceArray.length); i++) {
24
+ const traceItem = traceArray[i];
25
+ if (traceItem?.file && traceItem?.line) traces.push(`${traceItem.file}:${traceItem.line}`);
26
+ }
27
+ if (error.stack && traces.length === 0) traces.push(error.stack);
28
+ return {
29
+ traces,
30
+ traceAsString: traces.map(nl2br).join("<br>")
31
+ };
32
+ };
33
+ var getErrorMessage = function(error, maxTraceLines = 5) {
34
+ const { traces, traceAsString } = getErrorTrace(error, maxTraceLines);
35
+ return {
36
+ heading: getErrorHeading(error),
37
+ text: getErrorText(error),
38
+ trace: traceAsString,
39
+ traceAsString,
40
+ traceAsArray: traces
41
+ };
42
+ };
43
+ //#endregion
44
+ export { getErrorMessage };
45
+
46
+ //# sourceMappingURL=forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms.js","names":[],"sources":["../../src/utils/forms.ts"],"sourcesContent":["interface ErrorContent {\n heading: string;\n text: string;\n trace: string;\n traceAsString: string;\n traceAsArray: string[];\n}\n\ninterface ServerError {\n response?: {\n statusText?: string;\n data?: {\n message?: string;\n error?: string;\n file?: string;\n line?: string;\n trace?: Array<{\n file?: string;\n line?: string;\n }>;\n };\n };\n message?: string;\n stack?: string;\n}\n\nconst nl2br = (str: string): string => {\n return str.replace(/\\n/g, '<br>');\n};\n\nconst getErrorHeading = (error: ServerError): string => {\n // Server error status text (e.g., \"Internal Server Error\", \"Bad Request\")\n if (error.response?.statusText) {\n return error.response.statusText;\n }\n\n // Network errors or other client-side errors\n if (error.message?.includes('Network Error')) {\n return 'Network Error';\n }\n\n if (error.message?.includes('timeout')) {\n return 'Request Timeout';\n }\n\n // Default fallback\n return 'An error has occurred';\n};\n\nconst getErrorText = (error: ServerError): string => {\n // Server-side error messages\n if (error.response?.data?.message) {\n return error.response.data.message;\n }\n\n if (error.response?.data?.error) {\n return error.response.data.error;\n }\n\n // Client-side error messages\n if (error.message) {\n return error.message;\n }\n\n // Fallback to string representation\n return String(error);\n};\n\nconst getErrorTrace = (error: ServerError, maxTraceLines: number = 5): { traces: string[], traceAsString: string } => {\n const traces: string[] = [];\n\n // Server-side file and line information\n const file1 = error.response?.data?.file;\n const line1 = error.response?.data?.line;\n\n if (file1 && line1) {\n traces.push(`${file1}:${line1}`);\n }\n\n // Server-side stack trace (up to maxTraceLines)\n const traceArray = error.response?.data?.trace || [];\n\n for (let i = 0; i < Math.min(maxTraceLines, traceArray.length); i++) {\n const traceItem = traceArray[i];\n\n if (traceItem?.file && traceItem?.line) {\n traces.push(`${traceItem.file}:${traceItem.line}`);\n }\n }\n\n // Client-side stack trace - only if there's no server-side trace\n if (error.stack && traces.length === 0) {\n traces.push(error.stack);\n }\n\n return {\n traces,\n traceAsString: traces.map(nl2br).join('<br>'),\n };\n};\n\nexport const getErrorMessage = function(error: ServerError, maxTraceLines: number = 5): ErrorContent {\n const { traces, traceAsString } = getErrorTrace(error, maxTraceLines);\n\n return {\n heading: getErrorHeading(error),\n text: getErrorText(error),\n trace: traceAsString, // Keep for backward compatibility\n traceAsString,\n traceAsArray: traces,\n };\n};\n"],"mappings":";AA0BA,IAAM,SAAS,QAAwB;AACnC,QAAO,IAAI,QAAQ,OAAO,OAAO;;AAGrC,IAAM,mBAAmB,UAA+B;AAEpD,KAAI,MAAM,UAAU,WAChB,QAAO,MAAM,SAAS;AAI1B,KAAI,MAAM,SAAS,SAAS,gBAAgB,CACxC,QAAO;AAGX,KAAI,MAAM,SAAS,SAAS,UAAU,CAClC,QAAO;AAIX,QAAO;;AAGX,IAAM,gBAAgB,UAA+B;AAEjD,KAAI,MAAM,UAAU,MAAM,QACtB,QAAO,MAAM,SAAS,KAAK;AAG/B,KAAI,MAAM,UAAU,MAAM,MACtB,QAAO,MAAM,SAAS,KAAK;AAI/B,KAAI,MAAM,QACN,QAAO,MAAM;AAIjB,QAAO,OAAO,MAAM;;AAGxB,IAAM,iBAAiB,OAAoB,gBAAwB,MAAmD;CAClH,MAAM,SAAmB,EAAE;CAG3B,MAAM,QAAQ,MAAM,UAAU,MAAM;CACpC,MAAM,QAAQ,MAAM,UAAU,MAAM;AAEpC,KAAI,SAAS,MACT,QAAO,KAAK,GAAG,MAAM,GAAG,QAAQ;CAIpC,MAAM,aAAa,MAAM,UAAU,MAAM,SAAS,EAAE;AAEpD,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,IAAI,eAAe,WAAW,OAAO,EAAE,KAAK;EACjE,MAAM,YAAY,WAAW;AAE7B,MAAI,WAAW,QAAQ,WAAW,KAC9B,QAAO,KAAK,GAAG,UAAU,KAAK,GAAG,UAAU,OAAO;;AAK1D,KAAI,MAAM,SAAS,OAAO,WAAW,EACjC,QAAO,KAAK,MAAM,MAAM;AAG5B,QAAO;EACH;EACA,eAAe,OAAO,IAAI,MAAM,CAAC,KAAK,OAAO;EAChD;;AAGL,IAAa,kBAAkB,SAAS,OAAoB,gBAAwB,GAAiB;CACjG,MAAM,EAAE,QAAQ,kBAAkB,cAAc,OAAO,cAAc;AAErE,QAAO;EACH,SAAS,gBAAgB,MAAM;EAC/B,MAAM,aAAa,MAAM;EACzB,OAAO;EACP;EACA,cAAc;EACjB"}
@@ -0,0 +1,69 @@
1
+ import { findUniqueHandle, generateHandle } from "./string.js";
2
+ //#region src/utils/handle.ts
3
+ /** Minimal dot/bracket path getter so this stays dependency-free (no lodash). */
4
+ var getByPath = (source, path) => {
5
+ if (source == null || typeof source !== "object" || !path) return;
6
+ const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
7
+ let current = source;
8
+ for (const segment of segments) {
9
+ if (current == null || typeof current !== "object") return;
10
+ current = current[segment];
11
+ }
12
+ return current;
13
+ };
14
+ var normalizeHandleSource = (value) => {
15
+ if (value == null) return "";
16
+ return String(value).replace(/\{[^}]*\}/g, " ").replace(/\s+/g, " ").trim();
17
+ };
18
+ /**
19
+ * Resolve dynamic reserved handles from other field values (e.g. a sibling field whose
20
+ * value would collide once slugified).
21
+ */
22
+ var getDynamicReservedHandles = (values, reservedFieldValues = []) => {
23
+ const dynamicHandles = [];
24
+ reservedFieldValues.forEach((fieldPath) => {
25
+ const value = getByPath(values, fieldPath);
26
+ if (value && typeof value === "string") {
27
+ const handleValue = generateHandle(normalizeHandleSource(value));
28
+ if (handleValue) dynamicHandles.push(handleValue);
29
+ }
30
+ });
31
+ return dynamicHandles;
32
+ };
33
+ var truncateHandle = (handle, maxLength) => {
34
+ if (!Number.isFinite(maxLength)) return handle;
35
+ const length = Math.max(Number(maxLength), 0);
36
+ return handle.slice(0, length);
37
+ };
38
+ var findUniqueHandleWithinMaxLength = (baseHandle, reservedHandles = [], maxLength) => {
39
+ if (!baseHandle) return "";
40
+ if (!Number.isFinite(maxLength)) return findUniqueHandle(baseHandle, reservedHandles);
41
+ const normalizedReserved = new Set((reservedHandles || []).map((handle) => {
42
+ return String(handle || "").toLowerCase();
43
+ }));
44
+ const truncatedBase = truncateHandle(baseHandle, maxLength);
45
+ if (!truncatedBase) return "";
46
+ if (!normalizedReserved.has(truncatedBase.toLowerCase())) return truncatedBase;
47
+ let suffix = 1;
48
+ while (suffix < 1e4) {
49
+ const suffixText = String(suffix);
50
+ const baseMaxLength = Math.max(Number(maxLength) - suffixText.length, 0);
51
+ const truncatedWithSuffix = `${truncatedBase.slice(0, baseMaxLength)}${suffixText}`;
52
+ if (!normalizedReserved.has(truncatedWithSuffix.toLowerCase())) return truncatedWithSuffix;
53
+ suffix += 1;
54
+ }
55
+ return truncatedBase;
56
+ };
57
+ /**
58
+ * Generate a unique handle from a human-readable source value, honouring reserved handles
59
+ * (static + dynamically derived from other field values) and an optional max length.
60
+ */
61
+ var buildUniqueHandleFromSource = ({ sourceValue, values = {}, reservedHandles = [], reservedFieldValues = [], maxLength }) => {
62
+ const baseHandle = generateHandle(normalizeHandleSource(sourceValue));
63
+ const dynamicHandles = getDynamicReservedHandles(values, reservedFieldValues);
64
+ return findUniqueHandleWithinMaxLength(baseHandle, [...reservedHandles, ...dynamicHandles], maxLength);
65
+ };
66
+ //#endregion
67
+ export { buildUniqueHandleFromSource, getDynamicReservedHandles };
68
+
69
+ //# sourceMappingURL=handle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handle.js","names":[],"sources":["../../src/utils/handle.ts"],"sourcesContent":["import { findUniqueHandle, generateHandle } from './string';\n\n/** Minimal dot/bracket path getter so this stays dependency-free (no lodash). */\nconst getByPath = (source: unknown, path: string): unknown => {\n if (source == null || typeof source !== 'object' || !path) {\n return undefined;\n }\n\n const segments = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.').filter(Boolean);\n let current: unknown = source;\n\n for (const segment of segments) {\n if (current == null || typeof current !== 'object') {\n return undefined;\n }\n\n current = (current as Record<string, unknown>)[segment];\n }\n\n return current;\n};\n\nconst normalizeHandleSource = (value: unknown): string => {\n if (value == null) {\n return '';\n }\n\n return String(value)\n // Strip variable/template tokens so handle generation uses human text only.\n .replace(/\\{[^}]*\\}/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\n/**\n * Resolve dynamic reserved handles from other field values (e.g. a sibling field whose\n * value would collide once slugified).\n */\nexport const getDynamicReservedHandles = (\n values: Record<string, unknown>,\n reservedFieldValues: string[] = [],\n): string[] => {\n const dynamicHandles: string[] = [];\n\n reservedFieldValues.forEach((fieldPath) => {\n const value = getByPath(values, fieldPath);\n\n if (value && typeof value === 'string') {\n const handleValue = generateHandle(normalizeHandleSource(value));\n\n if (handleValue) {\n dynamicHandles.push(handleValue);\n }\n }\n });\n\n return dynamicHandles;\n};\n\nconst truncateHandle = (handle: string, maxLength?: number): string => {\n if (!Number.isFinite(maxLength)) {\n return handle;\n }\n\n const length = Math.max(Number(maxLength), 0);\n return handle.slice(0, length);\n};\n\nconst findUniqueHandleWithinMaxLength = (\n baseHandle: string,\n reservedHandles: string[] = [],\n maxLength?: number,\n): string => {\n if (!baseHandle) {\n return '';\n }\n\n if (!Number.isFinite(maxLength)) {\n return findUniqueHandle(baseHandle, reservedHandles);\n }\n\n const normalizedReserved = new Set((reservedHandles || []).map((handle) => {\n return String(handle || '').toLowerCase();\n }));\n\n const truncatedBase = truncateHandle(baseHandle, maxLength);\n if (!truncatedBase) {\n return '';\n }\n\n if (!normalizedReserved.has(truncatedBase.toLowerCase())) {\n return truncatedBase;\n }\n\n let suffix = 1;\n\n while (suffix < 10000) {\n const suffixText = String(suffix);\n const baseMaxLength = Math.max(Number(maxLength) - suffixText.length, 0);\n const truncatedWithSuffix = `${truncatedBase.slice(0, baseMaxLength)}${suffixText}`;\n\n if (!normalizedReserved.has(truncatedWithSuffix.toLowerCase())) {\n return truncatedWithSuffix;\n }\n\n suffix += 1;\n }\n\n return truncatedBase;\n};\n\n/**\n * Generate a unique handle from a human-readable source value, honouring reserved handles\n * (static + dynamically derived from other field values) and an optional max length.\n */\nexport const buildUniqueHandleFromSource = ({\n sourceValue,\n values = {},\n reservedHandles = [],\n reservedFieldValues = [],\n maxLength,\n}: {\n sourceValue: unknown;\n values?: Record<string, unknown>;\n reservedHandles?: string[];\n reservedFieldValues?: string[];\n maxLength?: number;\n}): string => {\n const baseHandle = generateHandle(normalizeHandleSource(sourceValue));\n const dynamicHandles = getDynamicReservedHandles(values, reservedFieldValues);\n const allReservedHandles = [...reservedHandles, ...dynamicHandles];\n\n return findUniqueHandleWithinMaxLength(baseHandle, allReservedHandles, maxLength);\n};\n"],"mappings":";;;AAGA,IAAM,aAAa,QAAiB,SAA0B;AAC1D,KAAI,UAAU,QAAQ,OAAO,WAAW,YAAY,CAAC,KACjD;CAGJ,MAAM,WAAW,KAAK,QAAQ,cAAc,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,QAAQ;CAC7E,IAAI,UAAmB;AAEvB,MAAK,MAAM,WAAW,UAAU;AAC5B,MAAI,WAAW,QAAQ,OAAO,YAAY,SACtC;AAGJ,YAAW,QAAoC;;AAGnD,QAAO;;AAGX,IAAM,yBAAyB,UAA2B;AACtD,KAAI,SAAS,KACT,QAAO;AAGX,QAAO,OAAO,MAAM,CAEf,QAAQ,cAAc,IAAI,CAC1B,QAAQ,QAAQ,IAAI,CACpB,MAAM;;;;;;AAOf,IAAa,6BACT,QACA,sBAAgC,EAAE,KACvB;CACX,MAAM,iBAA2B,EAAE;AAEnC,qBAAoB,SAAS,cAAc;EACvC,MAAM,QAAQ,UAAU,QAAQ,UAAU;AAE1C,MAAI,SAAS,OAAO,UAAU,UAAU;GACpC,MAAM,cAAc,eAAe,sBAAsB,MAAM,CAAC;AAEhE,OAAI,YACA,gBAAe,KAAK,YAAY;;GAG1C;AAEF,QAAO;;AAGX,IAAM,kBAAkB,QAAgB,cAA+B;AACnE,KAAI,CAAC,OAAO,SAAS,UAAU,CAC3B,QAAO;CAGX,MAAM,SAAS,KAAK,IAAI,OAAO,UAAU,EAAE,EAAE;AAC7C,QAAO,OAAO,MAAM,GAAG,OAAO;;AAGlC,IAAM,mCACF,YACA,kBAA4B,EAAE,EAC9B,cACS;AACT,KAAI,CAAC,WACD,QAAO;AAGX,KAAI,CAAC,OAAO,SAAS,UAAU,CAC3B,QAAO,iBAAiB,YAAY,gBAAgB;CAGxD,MAAM,qBAAqB,IAAI,KAAK,mBAAmB,EAAE,EAAE,KAAK,WAAW;AACvE,SAAO,OAAO,UAAU,GAAG,CAAC,aAAa;GAC3C,CAAC;CAEH,MAAM,gBAAgB,eAAe,YAAY,UAAU;AAC3D,KAAI,CAAC,cACD,QAAO;AAGX,KAAI,CAAC,mBAAmB,IAAI,cAAc,aAAa,CAAC,CACpD,QAAO;CAGX,IAAI,SAAS;AAEb,QAAO,SAAS,KAAO;EACnB,MAAM,aAAa,OAAO,OAAO;EACjC,MAAM,gBAAgB,KAAK,IAAI,OAAO,UAAU,GAAG,WAAW,QAAQ,EAAE;EACxE,MAAM,sBAAsB,GAAG,cAAc,MAAM,GAAG,cAAc,GAAG;AAEvE,MAAI,CAAC,mBAAmB,IAAI,oBAAoB,aAAa,CAAC,CAC1D,QAAO;AAGX,YAAU;;AAGd,QAAO;;;;;;AAOX,IAAa,+BAA+B,EACxC,aACA,SAAS,EAAE,EACX,kBAAkB,EAAE,EACpB,sBAAsB,EAAE,EACxB,gBAOU;CACV,MAAM,aAAa,eAAe,sBAAsB,YAAY,CAAC;CACrE,MAAM,iBAAiB,0BAA0B,QAAQ,oBAAoB;AAG7E,QAAO,gCAAgC,YAAY,CAFvB,GAAG,iBAAiB,GAAG,eAEA,EAAoB,UAAU"}
@@ -1,11 +1,27 @@
1
1
  /**
2
- * Initialize markdown-it instance with secure defaults and common options
2
+ * Lazily construct the markdown-it instance.
3
+ *
4
+ * Instantiating markdown-it at module scope is a live side effect: bundlers must
5
+ * retain the ~2.3 MB dependency in any consumer that touches this module's graph,
6
+ * even one that only imported a sibling util (e.g. `generateHandle`). Deferring the
7
+ * `new MarkdownIt()` to first render keeps markdown-it out of the graph unless a
8
+ * `render*` helper is actually called.
9
+ *
10
+ * markdown-it is configured with secure defaults:
3
11
  * - HTML is disabled for security
4
12
  * - Links are auto-detected
5
13
  * - Typography features like smart quotes are enabled
6
14
  * - Line breaks are converted to <br> tags
7
15
  */
8
- declare const md: any;
16
+ declare function createMarkdownIt(): any;
17
+ /**
18
+ * Direct accessor to the shared markdown-it instance for advanced usage.
19
+ *
20
+ * Replaces the former eager `export { md }`: an exported pre-built instance is
21
+ * itself a module-level side effect and would defeat the lazy loading above.
22
+ * Call this only when you need the full markdown-it API.
23
+ */
24
+ export declare const getMarkdownIt: () => ReturnType<typeof createMarkdownIt>;
9
25
  /**
10
26
  * Renders markdown content as block-level HTML
11
27
  * Includes block elements like headers, paragraphs, lists etc.
@@ -22,9 +38,5 @@ export declare const renderMarkdown: (content: string) => string;
22
38
  * @returns Rendered HTML string, or empty string if no content provided
23
39
  */
24
40
  export declare const renderInlineMarkdown: (content: string) => string;
25
- /**
26
- * Export the configured markdown-it instance for direct usage
27
- * Allows access to the full markdown-it API if needed
28
- */
29
- export { md };
41
+ export {};
30
42
  //# sourceMappingURL=markdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/utils/markdown.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,QAAA,MAAM,EAAE,KAKN,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAIhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,MAItD,CAAC;AAEF;;;GAGG;AACH,OAAO,EAAE,EAAE,EAAE,CAAC"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/utils/markdown.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,iBAAS,gBAAgB,QAOxB;AAOD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAO,UAAU,CAAC,OAAO,gBAAgB,CAAY,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAIhD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,MAItD,CAAC"}
@@ -0,0 +1,61 @@
1
+ import MarkdownIt from "markdown-it";
2
+ //#region src/utils/markdown.ts
3
+ /**
4
+ * Lazily construct the markdown-it instance.
5
+ *
6
+ * Instantiating markdown-it at module scope is a live side effect: bundlers must
7
+ * retain the ~2.3 MB dependency in any consumer that touches this module's graph,
8
+ * even one that only imported a sibling util (e.g. `generateHandle`). Deferring the
9
+ * `new MarkdownIt()` to first render keeps markdown-it out of the graph unless a
10
+ * `render*` helper is actually called.
11
+ *
12
+ * markdown-it is configured with secure defaults:
13
+ * - HTML is disabled for security
14
+ * - Links are auto-detected
15
+ * - Typography features like smart quotes are enabled
16
+ * - Line breaks are converted to <br> tags
17
+ */
18
+ function createMarkdownIt() {
19
+ return new MarkdownIt({
20
+ html: false,
21
+ linkify: true,
22
+ typographer: true,
23
+ breaks: true
24
+ });
25
+ }
26
+ var mdInstance;
27
+ var getMd = () => mdInstance ??= createMarkdownIt();
28
+ /**
29
+ * Direct accessor to the shared markdown-it instance for advanced usage.
30
+ *
31
+ * Replaces the former eager `export { md }`: an exported pre-built instance is
32
+ * itself a module-level side effect and would defeat the lazy loading above.
33
+ * Call this only when you need the full markdown-it API.
34
+ */
35
+ var getMarkdownIt = () => getMd();
36
+ /**
37
+ * Renders markdown content as block-level HTML
38
+ * Includes block elements like headers, paragraphs, lists etc.
39
+ *
40
+ * @param content - The markdown string to render
41
+ * @returns Rendered HTML string, or empty string if no content provided
42
+ */
43
+ var renderMarkdown = (content) => {
44
+ if (!content) return "";
45
+ return getMd().render(content);
46
+ };
47
+ /**
48
+ * Renders markdown content as inline HTML only
49
+ * Excludes block-level elements, only processes inline markdown syntax
50
+ *
51
+ * @param content - The markdown string to render
52
+ * @returns Rendered HTML string, or empty string if no content provided
53
+ */
54
+ var renderInlineMarkdown = (content) => {
55
+ if (!content) return "";
56
+ return getMd().renderInline(content);
57
+ };
58
+ //#endregion
59
+ export { getMarkdownIt, renderInlineMarkdown, renderMarkdown };
60
+
61
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","names":[],"sources":["../../src/utils/markdown.ts"],"sourcesContent":["import MarkdownIt from 'markdown-it';\n\n/**\n * Lazily construct the markdown-it instance.\n *\n * Instantiating markdown-it at module scope is a live side effect: bundlers must\n * retain the ~2.3 MB dependency in any consumer that touches this module's graph,\n * even one that only imported a sibling util (e.g. `generateHandle`). Deferring the\n * `new MarkdownIt()` to first render keeps markdown-it out of the graph unless a\n * `render*` helper is actually called.\n *\n * markdown-it is configured with secure defaults:\n * - HTML is disabled for security\n * - Links are auto-detected\n * - Typography features like smart quotes are enabled\n * - Line breaks are converted to <br> tags\n */\nfunction createMarkdownIt() {\n return new MarkdownIt({\n html: false, // Disable HTML for security\n linkify: true, // Auto-detect links\n typographer: true, // Enable typographic replacements\n breaks: true, // Convert line breaks to <br>\n });\n}\n\n// Cached singleton — built on first use, reused thereafter.\nlet mdInstance: ReturnType<typeof createMarkdownIt> | undefined;\n\nconst getMd = () => (mdInstance ??= createMarkdownIt());\n\n/**\n * Direct accessor to the shared markdown-it instance for advanced usage.\n *\n * Replaces the former eager `export { md }`: an exported pre-built instance is\n * itself a module-level side effect and would defeat the lazy loading above.\n * Call this only when you need the full markdown-it API.\n */\nexport const getMarkdownIt = (): ReturnType<typeof createMarkdownIt> => getMd();\n\n/**\n * Renders markdown content as block-level HTML\n * Includes block elements like headers, paragraphs, lists etc.\n *\n * @param content - The markdown string to render\n * @returns Rendered HTML string, or empty string if no content provided\n */\nexport const renderMarkdown = (content: string): string => {\n if (!content) { return ''; }\n\n return getMd().render(content);\n};\n\n/**\n * Renders markdown content as inline HTML only\n * Excludes block-level elements, only processes inline markdown syntax\n *\n * @param content - The markdown string to render\n * @returns Rendered HTML string, or empty string if no content provided\n */\nexport const renderInlineMarkdown = (content: string): string => {\n if (!content) { return ''; }\n\n return getMd().renderInline(content);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAS,mBAAmB;AACxB,QAAO,IAAI,WAAW;EAClB,MAAM;EACN,SAAS;EACT,aAAa;EACb,QAAQ;EACX,CAAC;;AAIN,IAAI;AAEJ,IAAM,cAAe,eAAe,kBAAkB;;;;;;;;AAStD,IAAa,sBAA2D,OAAO;;;;;;;;AAS/E,IAAa,kBAAkB,YAA4B;AACvD,KAAI,CAAC,QAAW,QAAO;AAEvB,QAAO,OAAO,CAAC,OAAO,QAAQ;;;;;;;;;AAUlC,IAAa,wBAAwB,YAA4B;AAC7D,KAAI,CAAC,QAAW,QAAO;AAEvB,QAAO,OAAO,CAAC,aAAa,QAAQ"}
@@ -0,0 +1,36 @@
1
+ //#region src/utils/promises.ts
2
+ /**
3
+ * Creates a function that ensures a promise takes at least a minimum amount of time to resolve
4
+ *
5
+ * @param ms - The minimum time in milliseconds that the promise should take
6
+ * @returns A function that wraps a promise or promise-returning function
7
+ * @example
8
+ * ```ts
9
+ * const slowFetch = takeAtLeast(1000)(fetch('https://api.example.com'));
10
+ * // Will take at least 1 second even if the fetch is faster
11
+ * ```
12
+ */
13
+ var takeAtLeast = function(ms) {
14
+ /**
15
+ * Wraps a promise or promise-returning function to ensure minimum execution time
16
+ *
17
+ * @param promiseOrFn - A promise or a function that returns a promise
18
+ * @returns A promise that resolves with the original promise's value after at least `ms` milliseconds
19
+ * @throws Will throw if the original promise rejects
20
+ */
21
+ return function(promiseOrFn) {
22
+ const promise = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
23
+ const delay = new Promise((resolve) => {
24
+ return setTimeout(resolve, ms);
25
+ });
26
+ return Promise.allSettled([promise, delay]).then((results) => {
27
+ const [promiseResult] = results;
28
+ if (promiseResult.status === "rejected") throw promiseResult.reason;
29
+ return promiseResult.value;
30
+ });
31
+ };
32
+ };
33
+ //#endregion
34
+ export { takeAtLeast };
35
+
36
+ //# sourceMappingURL=promises.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promises.js","names":[],"sources":["../../src/utils/promises.ts"],"sourcesContent":["/**\n * Creates a function that ensures a promise takes at least a minimum amount of time to resolve\n *\n * @param ms - The minimum time in milliseconds that the promise should take\n * @returns A function that wraps a promise or promise-returning function\n * @example\n * ```ts\n * const slowFetch = takeAtLeast(1000)(fetch('https://api.example.com'));\n * // Will take at least 1 second even if the fetch is faster\n * ```\n */\nexport const takeAtLeast = function(ms: number) {\n /**\n * Wraps a promise or promise-returning function to ensure minimum execution time\n *\n * @param promiseOrFn - A promise or a function that returns a promise\n * @returns A promise that resolves with the original promise's value after at least `ms` milliseconds\n * @throws Will throw if the original promise rejects\n */\n return function <T>(promiseOrFn: Promise<T> | (() => Promise<T>)): Promise<T> {\n const promise = typeof promiseOrFn === 'function' ? promiseOrFn() : promiseOrFn;\n const delay = new Promise<void>((resolve) => { return setTimeout(resolve, ms); });\n\n return Promise.allSettled([promise, delay]).then((results) => {\n const [promiseResult] = results;\n\n if (promiseResult.status === 'rejected') {\n throw promiseResult.reason;\n }\n\n return promiseResult.value;\n });\n };\n};\n"],"mappings":";;;;;;;;;;;;AAWA,IAAa,cAAc,SAAS,IAAY;;;;;;;;AAQ5C,QAAO,SAAa,aAA0D;EAC1E,MAAM,UAAU,OAAO,gBAAgB,aAAa,aAAa,GAAG;EACpE,MAAM,QAAQ,IAAI,SAAe,YAAY;AAAE,UAAO,WAAW,SAAS,GAAG;IAAI;AAEjF,SAAO,QAAQ,WAAW,CAAC,SAAS,MAAM,CAAC,CAAC,MAAM,YAAY;GAC1D,MAAM,CAAC,iBAAiB;AAExB,OAAI,cAAc,WAAW,WACzB,OAAM,cAAc;AAGxB,UAAO,cAAc;IACvB"}
@@ -0,0 +1,13 @@
1
+ //#region src/utils/query.ts
2
+ var getQueryParam = (key) => {
3
+ return new URLSearchParams(window.location.search).get(key);
4
+ };
5
+ var setQueryParam = (key, value) => {
6
+ const url = new URL(window.location.href);
7
+ url.searchParams.set(key, value);
8
+ window.history.replaceState({}, "", url.toString());
9
+ };
10
+ //#endregion
11
+ export { getQueryParam, setQueryParam };
12
+
13
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","names":[],"sources":["../../src/utils/query.ts"],"sourcesContent":["export const getQueryParam = (key: string): string | null => {\n const urlParams = new URLSearchParams(window.location.search);\n return urlParams.get(key);\n};\n\nexport const setQueryParam = (key: string, value: string): void => {\n const url = new URL(window.location.href);\n url.searchParams.set(key, value);\n window.history.replaceState({}, '', url.toString());\n};\n"],"mappings":";AAAA,IAAa,iBAAiB,QAA+B;AAEzD,QAAO,IADe,gBAAgB,OAAO,SAAS,OAC/C,CAAU,IAAI,IAAI;;AAG7B,IAAa,iBAAiB,KAAa,UAAwB;CAC/D,MAAM,MAAM,IAAI,IAAI,OAAO,SAAS,KAAK;AACzC,KAAI,aAAa,IAAI,KAAK,MAAM;AAChC,QAAO,QAAQ,aAAa,EAAE,EAAE,IAAI,IAAI,UAAU,CAAC"}