UNPKG

@glyphtek/unspecd

Version:

A declarative UI framework for building internal tools and dashboards with TypeScript. Create interactive tables, forms, and dashboards using simple specifications.

1,278 lines (1,272 loc) 81.8 kB
// @bun var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); // src/lib/data-handler.ts async function invokeDataSource({ specFunctions, functionName, params, onPending, onFulfilled, onRejected }) { onPending(); try { if (!specFunctions || typeof specFunctions !== "object") { throw new Error("Invalid specFunctions: expected an object containing function implementations"); } if (!functionName || typeof functionName !== "string") { throw new Error("Invalid functionName: expected a non-empty string"); } const targetFunction = specFunctions[functionName]; if (!targetFunction) { throw new Error(`Function '${functionName}' not found in spec. Available functions: ${Object.keys(specFunctions).join(", ")}`); } if (typeof targetFunction !== "function") { throw new Error(`'${functionName}' exists in spec but is not a function. Got: ${typeof targetFunction}`); } const result = await targetFunction(params); onFulfilled(result); } catch (error) { let processedError; if (error instanceof Error) { processedError = error; } else { processedError = new Error(`Function '${functionName}' failed with: ${String(error)}`); } onRejected(processedError); } } // src/lib/theme.ts var theme = { panel: "bg-white border border-gray-200/60 rounded-xl shadow-lg shadow-gray-100/50 p-8 space-y-6 backdrop-blur-sm", title: "text-3xl font-bold mb-6 text-gray-900 bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent", description: "text-gray-700 leading-relaxed", label: "font-semibold text-gray-800 min-w-0 sm:min-w-[120px] flex-shrink-0 text-sm tracking-wide", textInput: "w-full px-4 py-3 border border-gray-300/80 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 disabled:bg-gray-50/80 disabled:text-gray-500 transition-all duration-200 hover:border-gray-400/80", button: { primary: "bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-semibold py-3 px-6 rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:ring-offset-2 shadow-lg shadow-blue-600/25 hover:shadow-xl hover:shadow-blue-700/30 transform hover:-translate-y-0.5", disabled: "bg-gray-400 cursor-not-allowed text-gray-600", secondary: "bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 disabled:cursor-not-allowed text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2", danger: "bg-red-600 hover:bg-red-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2" }, feedback: { loading: "text-gray-500 animate-pulse", error: "text-red-600 p-4 border border-red-300 rounded-lg bg-red-50", success: "text-green-600 p-4 border border-green-300 rounded-lg bg-green-50", warning: "text-amber-600 p-4 border border-amber-300 rounded-lg bg-amber-50", info: "text-blue-600 p-4 border border-blue-300 rounded-lg bg-blue-50" }, data: { container: "space-y-3 bg-white border border-gray-200 rounded-lg p-4", field: "flex flex-col sm:flex-row sm:items-start gap-1 sm:gap-2 py-2 border-b border-gray-100 last:border-b-0", value: "text-gray-900 flex-1 break-words", emptyValue: "text-gray-400 italic flex-1", noData: "text-gray-500 italic p-4 bg-gray-50 border border-gray-200 rounded" }, layout: { spacing: "space-y-4", container: "max-w-4xl mx-auto", grid: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" }, text: { xl: "text-xl font-semibold text-gray-900", lg: "text-lg font-medium text-gray-900", base: "text-base text-gray-700", sm: "text-sm text-gray-600", xs: "text-xs text-gray-500" } }; // src/lib/components/smart-form.ts async function renderSmartFormComponent(content, specFunctions, targetElement) { targetElement.innerHTML = ""; const loadingElement = document.createElement("div"); loadingElement.className = theme.feedback.loading; loadingElement.textContent = "Loading form configuration..."; targetElement.appendChild(loadingElement); try { const [initialData, fieldOptions] = await Promise.all([ loadInitialFormData(content, specFunctions), loadDynamicFieldOptions(content, specFunctions) ]); targetElement.innerHTML = ""; renderFormWithData(content, initialData, targetElement, specFunctions, fieldOptions); console.log("Smart form rendered successfully with", content.formConfig.fields.length, "fields"); if (initialData) { console.log("Form pre-populated with initial data:", initialData); } if (Object.keys(fieldOptions).length > 0) { console.log("Dynamic options loaded for fields:", Object.keys(fieldOptions)); } } catch (error) { targetElement.innerHTML = ""; const errorContainer = document.createElement("div"); errorContainer.className = theme.feedback.error; const errorTitle = document.createElement("strong"); errorTitle.textContent = "Failed to Load Form Configuration"; errorTitle.className = "block font-semibold mb-2"; const errorMessage = document.createElement("p"); errorMessage.textContent = error instanceof Error ? error.message : "Unknown error occurred"; errorMessage.className = "text-sm"; errorContainer.appendChild(errorTitle); errorContainer.appendChild(errorMessage); targetElement.appendChild(errorContainer); console.error("Form loading failed:", error); } } async function loadInitialFormData(content, specFunctions) { if (!content.dataLoader) { return null; } return new Promise((resolve, reject) => { invokeDataSource({ specFunctions, functionName: content.dataLoader.functionName, params: {}, onPending: () => { console.log(`Loading form data using function: ${content.dataLoader.functionName}`); }, onFulfilled: (data) => { resolve(data); }, onRejected: (error) => { reject(new Error(`Failed to load form data: ${error.message}`)); } }); }); } async function loadDynamicFieldOptions(content, specFunctions) { const fieldsWithDynamicOptions = content.formConfig.fields.filter((field) => field.editorOptions?.optionsLoader); if (fieldsWithDynamicOptions.length === 0) { return {}; } console.log(`Loading dynamic options for ${fieldsWithDynamicOptions.length} fields`); const optionsPromises = fieldsWithDynamicOptions.map((field) => { const optionsLoader = field.editorOptions.optionsLoader; return new Promise((resolve, reject) => { invokeDataSource({ specFunctions, functionName: optionsLoader.functionName, params: optionsLoader.params || {}, onPending: () => { console.log(`Loading options for field '${field.field}' using function: ${optionsLoader.functionName}`); }, onFulfilled: (options) => { const optionsArray = Array.isArray(options) ? options : []; resolve({ fieldName: field.field, options: optionsArray }); }, onRejected: (error) => { reject(new Error(`Failed to load options for field '${field.field}': ${error.message}`)); } }); }); }); const loadedOptions = await Promise.all(optionsPromises); const fieldOptions = {}; for (const { fieldName, options } of loadedOptions) { fieldOptions[fieldName] = options; } return fieldOptions; } function gatherCheckboxValue(fieldName) { const checkboxContainer = document.querySelector(`#field-${fieldName}`); if (checkboxContainer) { const checkbox = checkboxContainer.querySelector('input[type="checkbox"]'); return checkbox ? checkbox.checked : false; } return false; } function gatherRadioValue(fieldName) { const selectedRadio = document.querySelector(`input[name="${fieldName}"]:checked`); return selectedRadio ? selectedRadio.value : null; } function gatherNumberValue(fieldName) { const numberElement = document.querySelector(`[name="${fieldName}"]`); if (!numberElement) { console.warn(`Could not find number field with name: ${fieldName}`); return null; } const numValue = numberElement.value; return numValue ? Number.parseFloat(numValue) : null; } function gatherSelectValue(fieldName) { const selectElement = document.querySelector(`[name="${fieldName}"]`); if (!selectElement) { console.warn(`Could not find select field with name: ${fieldName}`); return null; } return selectElement.value || null; } function gatherTextValue(fieldName) { const textElement = document.querySelector(`[name="${fieldName}"]`); if (!textElement) { console.warn(`Could not find text field with name: ${fieldName}`); return null; } const textValue = textElement.value.trim(); return textValue || null; } function gatherFieldValue(fieldConfig) { const fieldName = fieldConfig.field; switch (fieldConfig.editorType) { case "checkbox": return gatherCheckboxValue(fieldName); case "radio": return gatherRadioValue(fieldName); case "number": return gatherNumberValue(fieldName); case "select": return gatherSelectValue(fieldName); default: return gatherTextValue(fieldName); } } function renderFormWithData(content, initialData, targetElement, specFunctions, fieldOptions = {}) { const form = document.createElement("form"); form.className = `${theme.panel} max-w-2xl mx-auto`; form.setAttribute("novalidate", ""); const fieldsContainer = document.createElement("div"); fieldsContainer.className = "space-y-6"; content.formConfig.fields.forEach((fieldConfig) => { const fieldValue = initialData ? initialData[fieldConfig.field] : undefined; const dynamicOptions = fieldOptions[fieldConfig.field]; const fieldContainer = renderFormField(fieldConfig, fieldValue, dynamicOptions); fieldsContainer.appendChild(fieldContainer); }); form.appendChild(fieldsContainer); const submitButtonContainer = document.createElement("div"); submitButtonContainer.className = "pt-6 border-t border-gray-200/60 mt-8"; const submitButton = document.createElement("button"); submitButton.type = "submit"; submitButton.textContent = content.formConfig.submitButton?.label || "Submit"; submitButton.className = theme.button.primary; submitButtonContainer.appendChild(submitButton); form.appendChild(submitButtonContainer); form.addEventListener("submit", async (event) => { event.preventDefault(); const formData = {}; content.formConfig.fields.forEach((fieldConfig) => { const fieldValue = gatherFieldValue(fieldConfig); if (fieldValue !== null) { formData[fieldConfig.field] = fieldValue; } }); console.log("Form data gathered:", formData); await invokeDataSource({ specFunctions, functionName: content.onSubmit.functionName, params: { formData, originalData: initialData }, onPending: () => { submitButton.disabled = true; submitButton.textContent = "Submitting..."; submitButton.className = submitButton.className.replace("bg-blue-600 hover:bg-blue-700", "bg-gray-400 cursor-not-allowed"); console.log("Form submission started"); }, onFulfilled: (response) => { resetSubmitButton(submitButton, content); const successMessage = processSuccessMessage(response); window.alert(successMessage); handleRedirect(content); console.log("Form submitted successfully:", response); }, onRejected: (error) => { resetSubmitButton(submitButton, content); window.alert(`Form submission failed: ${error.message}`); console.error("Form submission failed:", error); } }); }); targetElement.appendChild(form); } function createFormFieldLabel(fieldConfig) { const label = document.createElement("label"); label.textContent = fieldConfig.label; if (fieldConfig.required) { label.textContent += " *"; } label.className = `${theme.label} block`; return label; } function configureCheckboxInput(inputElement, fieldConfig, initialValue, label) { const checkbox = inputElement.querySelector('input[type="checkbox"]'); if (checkbox) { checkbox.setAttribute("name", fieldConfig.field); checkbox.id = `field-${fieldConfig.field}`; label.setAttribute("for", checkbox.id); if (fieldConfig.required) { checkbox.setAttribute("required", ""); } if (fieldConfig.disabled) { checkbox.setAttribute("disabled", ""); } const valueToSet = initialValue !== undefined ? initialValue : fieldConfig.defaultValue; if (valueToSet !== undefined && valueToSet) { checkbox.checked = true; } } } function configureRadioInput(inputElement, fieldConfig, initialValue) { const valueToSet = initialValue !== undefined ? initialValue : fieldConfig.defaultValue; if (valueToSet !== undefined) { const radioToCheck = inputElement.querySelector(`input[value="${valueToSet}"]`); if (radioToCheck) { radioToCheck.checked = true; } } } function configureRegularInput(inputElement, fieldConfig, initialValue, label) { inputElement.setAttribute("name", fieldConfig.field); label.setAttribute("for", inputElement.id); if (fieldConfig.required) { inputElement.setAttribute("required", ""); } if (fieldConfig.disabled) { inputElement.setAttribute("disabled", ""); } if (fieldConfig.isReadOnly) { inputElement.setAttribute("readonly", ""); } const valueToSet = initialValue !== undefined ? initialValue : fieldConfig.defaultValue; if (valueToSet !== undefined) { if (inputElement instanceof HTMLInputElement || inputElement instanceof HTMLTextAreaElement || inputElement instanceof HTMLSelectElement) { inputElement.value = String(valueToSet); } } if (fieldConfig.placeholder && "placeholder" in inputElement) { inputElement.placeholder = fieldConfig.placeholder; } } function createHelpText(fieldConfig) { if (fieldConfig.helpText) { const helpText = document.createElement("p"); helpText.textContent = fieldConfig.helpText; helpText.className = `${theme.text.sm} text-gray-500`; return helpText; } return null; } function renderFormField(fieldConfig, initialValue, dynamicOptions) { const fieldContainer = document.createElement("div"); fieldContainer.className = "space-y-2"; const label = createFormFieldLabel(fieldConfig); const inputElement = createInputElement(fieldConfig, dynamicOptions); if (inputElement) { inputElement.id = `field-${fieldConfig.field}`; if (fieldConfig.editorType === "checkbox") { configureCheckboxInput(inputElement, fieldConfig, initialValue, label); } else if (fieldConfig.editorType === "radio") { configureRadioInput(inputElement, fieldConfig, initialValue); } else { configureRegularInput(inputElement, fieldConfig, initialValue, label); } } fieldContainer.appendChild(label); if (inputElement) { fieldContainer.appendChild(inputElement); } const helpText = createHelpText(fieldConfig); if (helpText) { fieldContainer.appendChild(helpText); } return fieldContainer; } function createInputElement(fieldConfig, dynamicOptions) { const { editorType } = fieldConfig; switch (editorType) { case "text": case "email": case "password": case "url": case "tel": case "number": case "date": case "datetime-local": { const input = document.createElement("input"); input.type = editorType; input.className = theme.textInput; return input; } case "textarea": { const textarea = document.createElement("textarea"); textarea.className = `${theme.textInput} min-h-[100px] resize-vertical`; textarea.rows = 4; return textarea; } case "select": { const select = document.createElement("select"); select.className = theme.textInput; const defaultOption = document.createElement("option"); defaultOption.value = ""; defaultOption.textContent = "Select an option..."; select.appendChild(defaultOption); const optionsToUse = dynamicOptions || fieldConfig.options; if (optionsToUse) { optionsToUse.forEach((option) => { const optionElement = document.createElement("option"); optionElement.value = String(option.value); optionElement.textContent = option.label; select.appendChild(optionElement); }); } return select; } case "checkbox": { const checkboxContainer = document.createElement("div"); checkboxContainer.className = "flex items-center space-x-2"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"; const checkboxLabel = document.createElement("span"); checkboxLabel.textContent = "Enable"; checkboxLabel.className = theme.text.base; checkboxContainer.appendChild(checkbox); checkboxContainer.appendChild(checkboxLabel); return checkboxContainer; } case "radio": { const radioContainer = document.createElement("div"); radioContainer.className = "space-y-2"; const radioOptionsToUse = dynamicOptions || fieldConfig.options; if (radioOptionsToUse) { radioOptionsToUse.forEach((option, index) => { const radioOption = document.createElement("div"); radioOption.className = "flex items-center space-x-2"; const radio = document.createElement("input"); radio.type = "radio"; radio.name = fieldConfig.field; radio.value = String(option.value); radio.id = `${fieldConfig.field}-${index}`; radio.className = "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"; const radioLabel = document.createElement("label"); radioLabel.textContent = option.label; radioLabel.setAttribute("for", radio.id); radioLabel.className = `${theme.text.base} cursor-pointer`; radioOption.appendChild(radio); radioOption.appendChild(radioLabel); radioContainer.appendChild(radioOption); }); } return radioContainer; } default: console.warn(`Unsupported editorType: ${editorType}`); return null; } } function resetSubmitButton(submitButton, content) { submitButton.disabled = false; submitButton.textContent = content.formConfig.submitButton?.label || "Submit"; submitButton.className = theme.button.primary; } function processSuccessMessage(response) { let successMessage = "Form submitted successfully!"; if (response && typeof response === "object") { if (response.message) { successMessage = response.message; } else if (response.success && response.message) { successMessage = response.message; } } return successMessage; } function handleRedirect(content) { if (content.onSubmit.onSuccess?.redirect && content.onSubmit.onSuccess?.redirectUrl) { window.location.href = content.onSubmit.onSuccess.redirectUrl; } } // src/lib/components/smart-table.ts function normalizeColumns(columns) { return columns.map((column) => { if (typeof column === "string") { return { field: column, label: column.charAt(0).toUpperCase() + column.slice(1) }; } return column; }); } function renderSmartTableComponent(content, specFunctions, targetElement, getInputValues, setRefreshCallback) { targetElement.innerHTML = ""; const normalizedColumns = normalizeColumns(content.tableConfig.columns); let currentPage = 1; const pageSize = content.tableConfig.pagination?.defaultPageSize || 20; let sortBy = content.tableConfig.defaultSort?.field || ""; let sortDirection = content.tableConfig.defaultSort?.direction || "asc"; let editingRowId = null; let currentTableData = null; let isSaving = false; function handleSortClick(columnField) { if (sortBy === columnField) { sortDirection = sortDirection === "asc" ? "desc" : "asc"; } else { sortBy = columnField; sortDirection = "asc"; } fetchAndRenderPage(1); } function handleEditClick(itemId) { editingRowId = itemId; renderTableBody(); } function handleCancelClick() { editingRowId = null; isSaving = false; renderTableBody(); } async function handleSaveClick(itemId, originalItem) { if (!content.tableConfig.itemUpdater) { console.error("No itemUpdater function configured for editing"); return; } const row = document.querySelector(`tr[data-item-id="${itemId}"]`); if (!row) return; const changes = {}; let hasChanges = false; normalizedColumns.forEach((column) => { if (column.isEditable) { const input = row.querySelector(`input[data-field="${column.field}"], select[data-field="${column.field}"]`); if (input) { const newValue = input.value; const oldValue = String(originalItem[column.field] || ""); if (newValue !== oldValue) { changes[column.field] = newValue; hasChanges = true; } } } }); if (!hasChanges) { handleCancelClick(); return; } isSaving = true; renderTableBody(); await invokeDataSource({ specFunctions, functionName: content.tableConfig.itemUpdater.functionName, params: { itemId, changes, currentItem: originalItem }, onPending: () => { console.log(`Saving changes for item ${itemId}:`, changes); }, onFulfilled: (updatedItem) => { if (currentTableData?.items) { const itemIndex = currentTableData.items.findIndex((item) => item.id === itemId); if (itemIndex !== -1) { currentTableData.items[itemIndex] = updatedItem || { ...originalItem, ...changes }; } } editingRowId = null; isSaving = false; renderTableBody(); console.log("Item updated successfully:", updatedItem || changes); }, onRejected: (error) => { isSaving = false; renderTableBody(); window.alert(`Failed to save changes: ${error.message}`); console.error("Save operation failed:", error); } }); } function createTableCell(column, item, isEditing) { const td = document.createElement("td"); td.className = "px-6 py-4 whitespace-nowrap text-sm text-gray-900"; if (isEditing && column.isEditable) { const input = document.createElement("input"); input.type = "text"; input.value = String(item[column.field] || ""); input.setAttribute("data-field", column.field); input.className = `${theme.textInput} w-full`; td.appendChild(input); } else { const fieldValue = item[column.field]; if (fieldValue == null) { td.textContent = "\u2014"; td.className += " text-gray-400 italic"; } else { const displayValue = String(fieldValue); td.textContent = displayValue; if (displayValue.length > 50) { td.className += " truncate max-w-xs"; td.title = displayValue; } } } return td; } function createActionsCell(item, index, isEditing) { const actionsTd = document.createElement("td"); actionsTd.className = "px-6 py-4 whitespace-nowrap text-sm font-medium"; if (isEditing) { const buttonContainer = document.createElement("div"); buttonContainer.className = "flex space-x-2"; const saveButton = document.createElement("button"); saveButton.textContent = isSaving ? "Saving..." : "Save"; saveButton.className = theme.button.primary; saveButton.disabled = isSaving; if (isSaving) { saveButton.className = saveButton.className.replace("bg-blue-600 hover:bg-blue-700", "bg-gray-400 cursor-not-allowed"); } saveButton.addEventListener("click", () => { handleSaveClick(item.id || index, item); }); const cancelButton = document.createElement("button"); cancelButton.textContent = "Cancel"; cancelButton.className = theme.button.secondary; cancelButton.disabled = isSaving; if (isSaving) { cancelButton.className = cancelButton.className.replace("bg-gray-100 hover:bg-gray-200", "bg-gray-50 cursor-not-allowed"); } cancelButton.addEventListener("click", handleCancelClick); buttonContainer.appendChild(saveButton); buttonContainer.appendChild(cancelButton); actionsTd.appendChild(buttonContainer); } else { const editButton = document.createElement("button"); editButton.textContent = "Edit"; editButton.className = theme.button.secondary; editButton.addEventListener("click", () => { handleEditClick(item.id || index); }); actionsTd.appendChild(editButton); } return actionsTd; } function createTableRow(item, index) { const row = document.createElement("tr"); row.className = index % 2 === 0 ? "bg-white" : "bg-gray-50"; row.setAttribute("data-item-id", item.id || index); const isEditing = editingRowId === (item.id || index); normalizedColumns.forEach((column) => { const td = createTableCell(column, item, isEditing); row.appendChild(td); }); if (content.tableConfig.itemUpdater) { const actionsTd = createActionsCell(item, index, isEditing); row.appendChild(actionsTd); } return row; } function renderTableBody(tbody) { const tableBody = tbody || document.querySelector("#unspecd-table-body"); if (!tableBody || !currentTableData) { console.warn("Table body element not found or no data available"); return; } tableBody.innerHTML = ""; currentTableData.items.forEach((item, index) => { const row = createTableRow(item, index); tableBody.appendChild(row); }); } function createHeaderCell(column) { const th = document.createElement("th"); th.className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"; if (column.isSortable) { const sortButton = document.createElement("button"); sortButton.className = "hover:text-gray-700 focus:outline-none focus:text-gray-700 transition-colors duration-200"; let buttonText = column.label; if (sortBy === column.field) { buttonText += sortDirection === "asc" ? " \u25B2" : " \u25BC"; } sortButton.textContent = buttonText; sortButton.addEventListener("click", () => { handleSortClick(column.field); }); th.appendChild(sortButton); } else { th.textContent = column.label; } return th; } function createActionsHeaderCell() { const actionsTh = document.createElement("th"); actionsTh.className = "px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"; actionsTh.textContent = "Actions"; return actionsTh; } function validateTableData(data) { return data && Array.isArray(data.items); } function renderInvalidDataError() { const errorElement = document.createElement("div"); errorElement.className = theme.feedback.error; const errorTitle = document.createElement("strong"); errorTitle.textContent = "Invalid Data Format"; errorTitle.className = "block font-semibold mb-2"; const errorMessage = document.createElement("p"); errorMessage.textContent = "Expected data format: { items: any[], totalItems: number }"; errorMessage.className = "text-sm"; errorElement.appendChild(errorTitle); errorElement.appendChild(errorMessage); targetElement.appendChild(errorElement); } function renderEmptyDataState() { const noDataElement = document.createElement("div"); noDataElement.className = theme.data.noData; noDataElement.textContent = "No data available"; targetElement.appendChild(noDataElement); } function createTableStructure(data) { const tableContainer = document.createElement("div"); tableContainer.className = "overflow-x-auto bg-white border border-gray-200 rounded-lg shadow-sm"; const table = document.createElement("table"); table.className = "min-w-full divide-y divide-gray-200"; const thead = document.createElement("thead"); thead.className = "bg-gray-50"; const headerRow = document.createElement("tr"); normalizedColumns.forEach((column) => { const th = createHeaderCell(column); headerRow.appendChild(th); }); if (content.tableConfig.itemUpdater) { const actionsTh = createActionsHeaderCell(); headerRow.appendChild(actionsTh); } thead.appendChild(headerRow); table.appendChild(thead); currentTableData = data; const tbody = document.createElement("tbody"); tbody.className = "bg-white divide-y divide-gray-200"; tbody.id = "table-body"; table.appendChild(tbody); return { tableContainer, table, tbody }; } function createTableFooter(data) { const footerInfo = document.createElement("div"); footerInfo.className = "px-6 py-3 bg-gray-50 border-t border-gray-200 text-sm text-gray-700"; const totalText = data.totalItems !== undefined ? `Showing ${data.items.length} of ${data.totalItems} items` : `Showing ${data.items.length} items`; footerInfo.textContent = totalText; return footerInfo; } function renderCompleteTable(data) { const { tableContainer, table, tbody } = createTableStructure(data); renderTableBody(tbody); tableContainer.appendChild(table); const footerInfo = createTableFooter(data); tableContainer.appendChild(footerInfo); targetElement.appendChild(tableContainer); if (data.totalItems && data.totalItems > pageSize) { const totalPages = Math.ceil(data.totalItems / pageSize); renderPaginationControls(totalPages); } console.log("Table rendered successfully:", { items: data.items.length, totalItems: data.totalItems, currentPage, sortBy, sortDirection }); } async function fetchAndRenderPage(page) { currentPage = page; const params = { page: currentPage, pageSize, sortBy, sortDirection, ...getInputValues ? getInputValues() : {} }; await invokeDataSource({ specFunctions, functionName: content.dataLoader.functionName, params, onPending: () => { targetElement.innerHTML = ""; const loadingElement = document.createElement("p"); loadingElement.textContent = "Loading table data..."; loadingElement.className = theme.feedback.loading; targetElement.appendChild(loadingElement); console.log(`Loading table data using function: ${content.dataLoader.functionName}, page: ${page}`); }, onFulfilled: (data) => { targetElement.innerHTML = ""; if (!validateTableData(data)) { renderInvalidDataError(); return; } if (data.items.length === 0) { renderEmptyDataState(); return; } renderCompleteTable(data); }, onRejected: (error) => { targetElement.innerHTML = ""; const errorContainer = document.createElement("div"); errorContainer.className = theme.feedback.error; const errorTitle = document.createElement("strong"); errorTitle.textContent = "Failed to Load Table Data"; errorTitle.className = "block font-semibold mb-2"; const errorMessage = document.createElement("p"); errorMessage.textContent = error.message; errorMessage.className = "text-sm"; errorContainer.appendChild(errorTitle); errorContainer.appendChild(errorMessage); targetElement.appendChild(errorContainer); console.error("Smart Table Component Error:", error); } }); } function renderPaginationControls(totalPages) { const paginationContainer = document.createElement("div"); paginationContainer.className = "flex items-center justify-between px-6 py-4 bg-white border-t border-gray-200"; const pageInfo = document.createElement("span"); pageInfo.textContent = `Page ${currentPage} of ${totalPages}`; pageInfo.className = "text-sm text-gray-700"; const buttonContainer = document.createElement("div"); buttonContainer.className = "flex space-x-2"; const prevButton = document.createElement("button"); prevButton.textContent = "Previous"; prevButton.className = theme.button.primary; if (currentPage === 1) { prevButton.disabled = true; prevButton.className = prevButton.className.replace("bg-blue-600 hover:bg-blue-700", "bg-gray-400 cursor-not-allowed"); } prevButton.addEventListener("click", async () => { if (currentPage > 1) { await fetchAndRenderPage(currentPage - 1); } }); const nextButton = document.createElement("button"); nextButton.textContent = "Next"; nextButton.className = theme.button.primary; if (currentPage === totalPages) { nextButton.disabled = true; nextButton.className = nextButton.className.replace("bg-blue-600 hover:bg-blue-700", "bg-gray-400 cursor-not-allowed"); } nextButton.addEventListener("click", async () => { if (currentPage < totalPages) { await fetchAndRenderPage(currentPage + 1); } }); buttonContainer.appendChild(prevButton); buttonContainer.appendChild(nextButton); paginationContainer.appendChild(pageInfo); paginationContainer.appendChild(buttonContainer); targetElement.appendChild(paginationContainer); } if (setRefreshCallback) { setRefreshCallback(() => fetchAndRenderPage(1)); } fetchAndRenderPage(1); } // src/lib/components/streaming-table.ts async function renderStreamingTableComponent(content, _specFunctions, targetElement) { targetElement.innerHTML = ""; const tableContainer = document.createElement("div"); tableContainer.className = "bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden"; const statusBar = document.createElement("div"); statusBar.className = "px-4 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between"; statusBar.innerHTML = ` <div class="flex items-center space-x-2"> <div class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse" id="connection-indicator"></div> <span class="text-sm text-gray-600" id="status-text">Connecting to live feed...</span> </div> <div class="text-xs text-gray-500" id="row-count">0 rows</div> `; const table = document.createElement("table"); table.className = "w-full"; const thead = document.createElement("thead"); thead.className = "bg-gray-50"; const headerRow = document.createElement("tr"); content.tableConfig.columns.forEach((column) => { const th = document.createElement("th"); th.className = "px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"; th.textContent = column.label; if (column.width) { th.style.width = column.width; } headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); const tbody = document.createElement("tbody"); tbody.className = "bg-white divide-y divide-gray-200"; tbody.id = "streaming-table-body"; table.appendChild(tbody); tableContainer.appendChild(statusBar); tableContainer.appendChild(table); targetElement.appendChild(tableContainer); const rowData = new Map; let _unsubscribeFunction = null; function formatValue(value, formatter) { if (value == null) return "N/A"; switch (formatter?.toLowerCase()) { case "currency": return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(Number(value)); case "datetime": return new Date(value).toLocaleString(); case "date": return new Date(value).toLocaleDateString(); case "time": return new Date(value).toLocaleTimeString(); default: return String(value); } } function createTableRow(item) { const row = document.createElement("tr"); row.className = "hover:bg-gray-50 transition-colors duration-150"; row.setAttribute("data-row-id", item[content.tableConfig.rowIdentifier]); content.tableConfig.columns.forEach((column) => { const cell = document.createElement("td"); cell.className = "px-4 py-3 text-sm text-gray-900"; cell.textContent = formatValue(item[column.field], column.formatter); row.appendChild(cell); }); return row; } function updateRowCount() { const rowCountElement = document.getElementById("row-count"); if (rowCountElement) { const count = rowData.size; rowCountElement.textContent = `${count} row${count !== 1 ? "s" : ""}`; } } function highlightNewRow(row) { if (content.tableConfig.streamingOptions?.highlightNewRows) { row.classList.add("bg-blue-50", "border-l-4", "border-blue-400"); setTimeout(() => { row.classList.remove("bg-blue-50", "border-l-4", "border-blue-400"); }, 3000); } } function animateRowUpdate(row) { if (content.tableConfig.streamingOptions?.showUpdateAnimations) { row.classList.add("bg-yellow-50"); setTimeout(() => { row.classList.remove("bg-yellow-50"); }, 1000); } } function enforceMaxRows() { const maxRows = content.tableConfig.streamingOptions?.maxRows; if (maxRows && rowData.size > maxRows) { const rowsToRemove = Array.from(rowData.keys()).slice(0, rowData.size - maxRows); rowsToRemove.forEach((rowId) => { rowData.delete(rowId); const rowElement = tbody.querySelector(`[data-row-id="${rowId}"]`); if (rowElement) { rowElement.remove(); } }); } } const onData = (event) => { console.log("\uD83D\uDCCA Streaming event received:", event); switch (event.type) { case "add": { const item = event.item; const rowId = item[content.tableConfig.rowIdentifier]; rowData.set(rowId, item); const newRow = createTableRow(item); if (tbody.firstChild) { tbody.insertBefore(newRow, tbody.firstChild); } else { tbody.appendChild(newRow); } highlightNewRow(newRow); enforceMaxRows(); updateRowCount(); break; } case "update": { const { itemId, changes } = event; const existingItem = rowData.get(itemId); if (existingItem) { const updatedItem = { ...existingItem, ...changes }; rowData.set(itemId, updatedItem); const rowElement = tbody.querySelector(`[data-row-id="${itemId}"]`); if (rowElement) { const cells = rowElement.querySelectorAll("td"); content.tableConfig.columns.forEach((column, index) => { if (cells[index]) { cells[index].textContent = formatValue(updatedItem[column.field], column.formatter); } }); animateRowUpdate(rowElement); } } break; } case "delete": { const { itemId } = event; rowData.delete(itemId); const rowElement = tbody.querySelector(`[data-row-id="${itemId}"]`); if (rowElement) { rowElement.remove(); } updateRowCount(); break; } case "replace": { rowData.clear(); tbody.innerHTML = ""; event.items.forEach((item) => { const rowId = item[content.tableConfig.rowIdentifier]; rowData.set(rowId, item); const newRow = createTableRow(item); tbody.appendChild(newRow); }); updateRowCount(); break; } case "clear": { rowData.clear(); tbody.innerHTML = ""; updateRowCount(); break; } default: console.warn("Unknown streaming event type:", event.type); } }; const onError = (error) => { console.error("\u274C Streaming table error:", error); const statusText = document.getElementById("status-text"); const indicator = document.getElementById("connection-indicator"); if (statusText && indicator) { statusText.textContent = `Error: ${error.message}`; indicator.className = "w-2 h-2 bg-red-500 rounded-full"; } }; const onConnect = () => { console.log("\u2705 Streaming table connected"); const statusText = document.getElementById("status-text"); const indicator = document.getElementById("connection-indicator"); if (statusText && indicator) { statusText.textContent = "Connected to live feed"; indicator.className = "w-2 h-2 bg-green-500 rounded-full"; } }; const onDisconnect = () => { console.log("\u26A0\uFE0F Streaming table disconnected"); const statusText = document.getElementById("status-text"); const indicator = document.getElementById("connection-indicator"); if (statusText && indicator) { statusText.textContent = "Disconnected from live feed"; indicator.className = "w-2 h-2 bg-gray-400 rounded-full"; } }; try { const wsPort = window.location.port ? Number.parseInt(window.location.port) + 1 : 3001; const wsUrl = `ws://${window.location.hostname}:${wsPort}`; console.log(`\uD83D\uDD0C Connecting to WebSocket at ${wsUrl}`); const ws = new WebSocket(wsUrl); ws.onopen = () => { console.log("\uD83D\uDD0C WebSocket connected"); let toolId = ""; if (window.__UNSPECD_TOOLS_DATA__) { const tool = window.__UNSPECD_TOOLS_DATA__.tools.find((t) => t.functionNames.includes(content.dataSource.functionName)); if (tool) { toolId = tool.id; } } if (!toolId) { onError(new Error(`Could not find tool ID for function '${content.dataSource.functionName}'`)); return; } ws.send(JSON.stringify({ type: "start-stream", toolId, functionName: content.dataSource.functionName, params: {} })); }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); switch (message.type) { case "data": onData(message.event); break; case "error": onError(new Error(message.error)); break; case "connect": onConnect(); break; case "disconnect": onDisconnect(); break; default: console.warn("Unknown WebSocket message type:", message.type); } } catch (error) { onError(error instanceof Error ? error : new Error("Failed to parse WebSocket message")); } }; ws.onerror = (error) => { console.error("\uD83D\uDD0C WebSocket error:", error); onError(new Error("WebSocket connection error")); }; ws.onclose = () => { console.log("\uD83D\uDD0C WebSocket disconnected"); onDisconnect(); }; _unsubscribeFunction = () => { console.log("\uD83D\uDD0C Closing WebSocket connection"); ws.close(); }; console.log("\uD83D\uDE80 Streaming table initialized successfully"); } catch (error) { console.error("\u274C Failed to initialize streaming table:", error); onError(error instanceof Error ? error : new Error("Unknown initialization error")); } console.log("\uD83D\uDCCB Streaming table component rendered with", content.tableConfig.columns.length, "columns"); } // src/lib/runtime.ts async function renderDisplayRecordComponent(content, functions, targetElement) { const loadingElement = document.createElement("div"); loadingElement.className = "flex items-center justify-center p-8"; loadingElement.innerHTML = ` <div class="animate-pulse flex items-center space-x-2"> <div class="w-4 h-4 bg-blue-500 rounded-full animate-bounce"></div> <div class="w-4 h-4 bg-blue-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div> <div class="w-4 h-4 bg-blue-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div> <span class="ml-2 text-gray-600">Loading data...</span> </div> `; targetElement.appendChild(loadingElement); try { const dataLoaderFunction = functions[content.dataLoader.functionName]; if (!dataLoaderFunction) { throw new Error(`Data loader function '${content.dataLoader.functionName}' not found`); } const data = await dataLoaderFunction({}); targetElement.removeChild(loadingElement); if (!data) { const noDataElement = document.createElement("div"); noDataElement.className = "text-center p-8 text-gray-500"; noDataElement.textContent = "No data available"; targetElement.appendChild(noDataElement); return; } const recordContainer = document.createElement("div"); recordContainer.className = "bg-white rounded-lg border border-gray-200 shadow-sm p-6 space-y-4"; content.displayConfig.fields.forEach((fieldConfig) => { const fieldValue = data[fieldConfig.field]; const fieldContainer = document.createElement("div"); fieldContainer.className = "border-b border-gray-100 pb-3 last:border-b-0 last:pb-0"; const labelElement = document.createElement("dt"); labelElement.textContent = fieldConfig.label; labelElement.className = "text-sm font-medium text-gray-500 mb-1"; const valueElement = document.createElement("dd"); valueElement.className = "text-lg font-semibold text-gray-900"; let formattedValue = fieldValue; if (fieldConfig.formatter) { formattedValue = formatValue(fieldValue, fieldConfig.formatter); } if (typeof formattedValue === "number") { valueElement.textContent = formattedValue.toLocaleString(); } else { valueElement.textContent = String(formattedValue || "N/A"); } if (fieldConfig.field === "count" || typeof fieldValue === "number") { valueElement.classList.add("text-2xl", "text-blue-600"); } fieldContainer.appendChild(labelElement); fieldContainer.appendChild(valueElement); recordContainer.appendChild(fieldContainer); }); targetElement.appendChild(recordContainer); } catch (error) { if (loadingElement.parentNode) { targetElement.removeChild(loadingElement); } console.error("Error loading display record data:", error); const errorElement = document.createElement("div"); errorElement.className = "bg-red-50 border border-red-200 rounded-lg p-4"; errorElement.innerHTML = ` <div class="flex items-center"> <div class="text-red-600 font-medium">Failed to load data</div> </div> <div class="text-red-500 text-sm mt-1">${error instanceof Error ? error.message : "Unknown error occurred"}</div> `; targetElement.appendChild(errorElement); } } function formatValue(value, formatter) { if (value == null) return "N/A"; switch (formatter.toLowerCase()) { case "date": if (value instanceof Date) { return value.toLocaleDateString(); } return new Date(value).toLocaleDateString(); case "datetime": if (value instanceof Date) { return value.toLocaleString(); } return new Date(value).toLocaleString(); case "time": if (value instanceof Date) { return value.toLocaleTimeString(); } return new Date(value).toLocaleTimeString(); case "currency": return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(Number(value)); case "percentage": return new Intl.NumberFormat("en-US", { style: "percent", minimumFractionDigits: 1, maximumFractionDigits: 1 }).format(Number(value) / 100); case "uppercase": return String(value).toUpperCase(); case "lowercase": return String(value).toLowerCase(); case "capitalize": return String(value).charAt(0).toUpperCase() + String(value).slice(1).toLowerCase(); default: console.warn(`Unknown formatter: ${formatter}`); return value; } } function renderActionButtonComponent(_content, _functions, targetElement) { const placeholder = document.createElement("p"); placeholder.textContent = "Action Button Component Placeholder"; placeholder.className = "text-gray-600 italic p-4 border border-dashed border-gray-300 rounded"; targetElement.appendChild(placeholder); } function createInputLabel(fieldName, inputDef) { const label = document.createElement("label"); label.textContent = inputDef.label; label.className = "block text-sm font-medium text-gray-700 mb-1"; label.setAttribute("for", `input-${fieldName}`); return label; } fun