diff --git a/packages/plugin-aggrid/package.json b/packages/plugin-aggrid/package.json index db0c9ede..3a20f4a2 100644 --- a/packages/plugin-aggrid/package.json +++ b/packages/plugin-aggrid/package.json @@ -34,6 +34,7 @@ "dependencies": { "@object-ui/components": "workspace:*", "@object-ui/core": "workspace:*", + "@object-ui/fields": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", "@object-ui/data-objectstack": "workspace:*" diff --git a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx index 5b33d42a..66c76c66 100644 --- a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx +++ b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx @@ -18,12 +18,11 @@ import type { StatusPanelDef, GetContextMenuItemsParams, MenuItemDef, - IServerSideDatasource, - IServerSideGetRowsParams } from 'ag-grid-community'; -import type { DataSource, FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types'; +import type { FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types'; import type { ObjectAgGridImplProps } from './object-aggrid.types'; import { FIELD_TYPE_TO_FILTER_TYPE } from './object-aggrid.types'; +import { createFieldCellRenderer, createFieldCellEditor } from './field-renderers'; /** * ObjectAgGridImpl - Metadata-driven AG Grid implementation @@ -61,7 +60,6 @@ export default function ObjectAgGridImpl({ const [error, setError] = useState(null); const [objectSchema, setObjectSchema] = useState(null); const [rowData, setRowData] = useState([]); - const [totalCount, setTotalCount] = useState(0); // Fetch object metadata useEffect(() => { @@ -115,7 +113,6 @@ export default function ObjectAgGridImpl({ const result = await dataSource.find(objectName, queryParams); setRowData(result.data || []); - setTotalCount(result.total || 0); callbacks?.onDataLoaded?.(result.data || []); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); @@ -416,15 +413,6 @@ export default function ObjectAgGridImpl({ ); } -/** - * Escape HTML to prevent XSS attacks - */ -function escapeHtml(text: string): string { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - /** * Get filter type based on field metadata */ @@ -438,166 +426,76 @@ function getFilterType(field: FieldMetadata): string | boolean { /** * Apply field type-specific formatting to column definition + * Uses field widgets from @object-ui/fields for consistent rendering */ function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void { - switch (field.type) { - case 'boolean': - colDef.cellRenderer = (params: any) => { - if (params.value === true) return '✓ Yes'; - if (params.value === false) return '✗ No'; - return ''; - }; - break; - - case 'currency': - colDef.valueFormatter = (params: any) => { - if (params.value == null) return ''; - const currency = (field as any).currency || 'USD'; - const precision = (field as any).precision || 2; - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency, - minimumFractionDigits: precision, - maximumFractionDigits: precision, - }).format(params.value); - }; - break; - - case 'percent': - colDef.valueFormatter = (params: any) => { - if (params.value == null) return ''; - const precision = (field as any).precision || 2; - return `${(params.value * 100).toFixed(precision)}%`; - }; - break; - - case 'date': - colDef.valueFormatter = (params: any) => { - if (!params.value) return ''; - try { - const date = new Date(params.value); - if (isNaN(date.getTime())) return ''; - return date.toLocaleDateString(); - } catch { - return ''; - } - }; - break; - - case 'datetime': - colDef.valueFormatter = (params: any) => { - if (!params.value) return ''; - try { - const date = new Date(params.value); - if (isNaN(date.getTime())) return ''; - return date.toLocaleString(); - } catch { - return ''; - } - }; - break; - - case 'time': - colDef.valueFormatter = (params: any) => { - if (!params.value) return ''; - return params.value; - }; - break; - - case 'email': - colDef.cellRenderer = (params: any) => { - if (!params.value) return ''; - const escaped = escapeHtml(params.value); - return `${escaped}`; - }; - break; - - case 'url': - colDef.cellRenderer = (params: any) => { - if (!params.value) return ''; - const escaped = escapeHtml(params.value); - return `${escaped}`; - }; - break; - - case 'phone': - colDef.cellRenderer = (params: any) => { - if (!params.value) return ''; - const escaped = escapeHtml(params.value); - return `${escaped}`; - }; - break; - - case 'select': - colDef.valueFormatter = (params: any) => { - if (!params.value) return ''; - const options = (field as any).options || []; - const option = options.find((opt: any) => opt.value === params.value); - return option?.label || params.value; - }; - break; - - case 'lookup': - case 'master_detail': - colDef.valueFormatter = (params: any) => { - if (!params.value) return ''; - // Handle lookup values - could be an object or just an ID - if (typeof params.value === 'object') { - return params.value.name || params.value.label || params.value.id || ''; - } - return String(params.value); - }; - break; + // Define field types that should use field widgets for rendering + const fieldWidgetTypes = [ + 'text', 'textarea', 'number', 'currency', 'percent', + 'boolean', 'select', 'date', 'datetime', 'time', + 'email', 'phone', 'url', 'password', 'color', + 'rating', 'image', 'avatar', 'lookup', 'slider', 'code' + ]; + + // Use field widget renderer if the type is supported + if (fieldWidgetTypes.includes(field.type)) { + colDef.cellRenderer = createFieldCellRenderer(field); + + // Add cell editor for editable fields + if (colDef.editable) { + colDef.cellEditor = createFieldCellEditor(field); - case 'number': { - const precision = (field as any).precision; - if (precision !== undefined) { + // Configure editor based on field type + if (['date', 'datetime', 'select', 'lookup', 'color'].includes(field.type)) { + colDef.cellEditorPopup = true; + } + } + } else { + // Fallback to simple rendering for unsupported types + switch (field.type) { + case 'master_detail': colDef.valueFormatter = (params: any) => { - if (params.value == null) return ''; - return Number(params.value).toFixed(precision); + if (!params.value) return ''; + // Handle lookup values - could be an object or just an ID + if (typeof params.value === 'object') { + return params.value.name || params.value.label || params.value.id || ''; + } + return String(params.value); + }; + break; + + case 'object': + colDef.cellRenderer = () => { + const span = document.createElement('span'); + span.className = 'text-gray-500 italic'; + span.textContent = '[Object]'; + return span; + }; + break; + + case 'vector': + colDef.cellRenderer = () => { + const span = document.createElement('span'); + span.className = 'text-gray-500 italic'; + span.textContent = '[Vector]'; + return span; + }; + break; + + case 'grid': + colDef.cellRenderer = () => { + const span = document.createElement('span'); + span.className = 'text-gray-500 italic'; + span.textContent = '[Grid]'; + return span; + }; + break; + + default: + // Default text rendering + colDef.valueFormatter = (params: any) => { + return params.value != null ? String(params.value) : ''; }; - } - break; } - - case 'color': - colDef.cellRenderer = (params: any) => { - if (!params.value) return ''; - const escaped = escapeHtml(params.value); - return `
-
- ${escaped} -
`; - }; - break; - - case 'rating': - colDef.cellRenderer = (params: any) => { - if (params.value == null) return ''; - const max = (field as any).max || 5; - const stars = '⭐'.repeat(Math.min(params.value, max)); - return stars; - }; - break; - - case 'image': - colDef.cellRenderer = (params: any) => { - if (!params.value) return ''; - const url = typeof params.value === 'string' ? params.value : params.value.url; - if (!url) return ''; - const escapedUrl = escapeHtml(url); - return ``; - }; - break; - - case 'avatar': - colDef.cellRenderer = (params: any) => { - if (!params.value) return ''; - const url = typeof params.value === 'string' ? params.value : params.value.url; - if (!url) return ''; - const escapedUrl = escapeHtml(url); - return ``; - }; - break; } } diff --git a/packages/plugin-aggrid/src/field-renderers.test.tsx b/packages/plugin-aggrid/src/field-renderers.test.tsx new file mode 100644 index 00000000..952c5b2d --- /dev/null +++ b/packages/plugin-aggrid/src/field-renderers.test.tsx @@ -0,0 +1,383 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import type { ICellRendererParams, ICellEditorParams } from 'ag-grid-community'; +import type { FieldMetadata } from '@object-ui/types'; +import { + FieldWidgetCellRenderer, + FieldWidgetCellEditor, + createFieldCellRenderer, + createFieldCellEditor +} from './field-renderers'; + +describe('field-renderers', () => { + describe('FieldWidgetCellRenderer', () => { + let renderer: FieldWidgetCellRenderer; + + beforeEach(() => { + renderer = new FieldWidgetCellRenderer(); + }); + + afterEach(() => { + renderer.destroy(); + }); + + it('should initialize with a field widget for supported types', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test value', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(params); + + expect(renderer.eGui).toBeDefined(); + expect(renderer.eGui.className).toBe('field-widget-cell'); + expect(renderer.root).toBeDefined(); + }); + + it('should initialize with fallback for unsupported types', () => { + const field = { + name: 'testField', + label: 'Test Field', + type: 'unsupported_type', + } as unknown as FieldMetadata; + + const params = { + value: 'fallback value', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(params); + + expect(renderer.eGui).toBeDefined(); + expect(renderer.eGui.textContent).toBe('fallback value'); + expect(renderer.root).toBeNull(); + }); + + it('should handle null values in fallback mode', () => { + const field = { + name: 'testField', + label: 'Test Field', + type: 'unsupported_type', + } as unknown as FieldMetadata; + + const params = { + value: null, + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(params); + + expect(renderer.eGui.textContent).toBe(''); + }); + + it('should return the GUI element', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(params); + const gui = renderer.getGui(); + + expect(gui).toBe(renderer.eGui); + }); + + it('should refresh with new value for supported field type', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const initParams = { + value: 'initial value', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(initParams); + + const refreshParams = { + value: 'updated value', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + const result = renderer.refresh(refreshParams); + + expect(result).toBe(true); + }); + + it('should refresh with new value for unsupported field type', () => { + const field = { + name: 'testField', + label: 'Test Field', + type: 'unsupported_type', + } as unknown as FieldMetadata; + + const initParams = { + value: 'initial value', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(initParams); + + const refreshParams = { + value: 'updated value', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + const result = renderer.refresh(refreshParams); + + expect(result).toBe(true); + expect(renderer.eGui.textContent).toBe('updated value'); + }); + + it('should clean up root on destroy', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(params); + + const unmountSpy = vi.spyOn(renderer.root!, 'unmount'); + renderer.destroy(); + + expect(unmountSpy).toHaveBeenCalled(); + }); + + it('should handle destroy when root is null', () => { + const field = { + name: 'testField', + label: 'Test Field', + type: 'unsupported_type', + } as unknown as FieldMetadata; + + const params = { + value: 'test', + field, + } as ICellRendererParams & { field: FieldMetadata }; + + renderer.init(params); + + // Should not throw + expect(() => renderer.destroy()).not.toThrow(); + }); + }); + + describe('FieldWidgetCellEditor', () => { + let editor: FieldWidgetCellEditor; + + beforeEach(() => { + editor = new FieldWidgetCellEditor(); + }); + + afterEach(() => { + editor.destroy(); + }); + + it('should initialize with a field widget for supported types', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'initial value', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + editor.init(params); + + expect(editor.eGui).toBeDefined(); + expect(editor.eGui.className).toBe('field-widget-editor'); + expect(editor.root).toBeDefined(); + expect(editor.currentValue).toBe('initial value'); + }); + + it('should initialize with fallback input for unsupported types', () => { + const field = { + name: 'testField', + label: 'Test Field', + type: 'unsupported_type', + } as unknown as FieldMetadata; + + const params = { + value: 'fallback value', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + editor.init(params); + + expect(editor.eGui).toBeDefined(); + expect(editor.root).toBeNull(); + const input = editor.eGui.querySelector('input'); + expect(input).toBeDefined(); + expect(input?.value).toBe('fallback value'); + }); + + it('should return current value', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test value', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + editor.init(params); + + expect(editor.getValue()).toBe('test value'); + }); + + it('should return the GUI element', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + editor.init(params); + const gui = editor.getGui(); + + expect(gui).toBe(editor.eGui); + }); + + it('should return true for popup editors for specific field types', () => { + const popupTypes = ['date', 'datetime', 'select', 'lookup', 'color'] as const; + + popupTypes.forEach(type => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type, + }; + + const params = { + value: 'test', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + const testEditor = new FieldWidgetCellEditor(); + testEditor.init(params); + + expect(testEditor.isPopup()).toBe(true); + testEditor.destroy(); + }); + }); + + it('should return false for popup editors for non-popup field types', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + editor.init(params); + + expect(editor.isPopup()).toBe(false); + }); + + it('should clean up root on destroy', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const params = { + value: 'test', + field, + } as ICellEditorParams & { field: FieldMetadata }; + + editor.init(params); + + const unmountSpy = vi.spyOn(editor.root!, 'unmount'); + editor.destroy(); + + expect(unmountSpy).toHaveBeenCalled(); + }); + }); + + describe('createFieldCellRenderer', () => { + it('should create a renderer class with field metadata', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const RendererClass = createFieldCellRenderer(field); + const renderer = new RendererClass(); + + const params = { + value: 'test value', + } as ICellRendererParams; + + renderer.init(params); + + expect(renderer.eGui).toBeDefined(); + renderer.destroy(); + }); + }); + + describe('createFieldCellEditor', () => { + it('should create an editor class with field metadata', () => { + const field: FieldMetadata = { + name: 'testField', + label: 'Test Field', + type: 'text', + }; + + const EditorClass = createFieldCellEditor(field); + const editor = new EditorClass(); + + const params = { + value: 'test value', + } as ICellEditorParams; + + editor.init(params); + + expect(editor.eGui).toBeDefined(); + editor.destroy(); + }); + }); +}); diff --git a/packages/plugin-aggrid/src/field-renderers.tsx b/packages/plugin-aggrid/src/field-renderers.tsx new file mode 100644 index 00000000..b0fa34c5 --- /dev/null +++ b/packages/plugin-aggrid/src/field-renderers.tsx @@ -0,0 +1,224 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import type { ICellRendererParams, ICellEditorParams } from 'ag-grid-community'; +import type { FieldMetadata } from '@object-ui/types'; + +// Import field widgets +import { + TextField, + NumberField, + BooleanField, + SelectField, + DateField, + DateTimeField, + TimeField, + EmailField, + PhoneField, + UrlField, + CurrencyField, + PercentField, + PasswordField, + TextAreaField, + ColorField, + RatingField, + ImageField, + AvatarField, + LookupField, + SliderField, + CodeField, +} from '@object-ui/fields'; + +/** + * Map field type to field widget component + * Defined at module level to avoid recreating on every call + */ +const widgetMap: Record> = { + text: TextField, + textarea: TextAreaField, + number: NumberField, + currency: CurrencyField, + percent: PercentField, + boolean: BooleanField, + select: SelectField, + date: DateField, + datetime: DateTimeField, + time: TimeField, + email: EmailField, + phone: PhoneField, + url: UrlField, + password: PasswordField, + color: ColorField, + rating: RatingField, + image: ImageField, + avatar: AvatarField, + lookup: LookupField, + slider: SliderField, + code: CodeField, +}; + +function getFieldWidget(fieldType: string): React.ComponentType | null { + return widgetMap[fieldType] || null; +} + +/** + * AG Grid Cell Renderer using Field Widgets (Read-only mode) + */ +export class FieldWidgetCellRenderer { + public eGui!: HTMLDivElement; + public root: Root | null = null; + + init(params: ICellRendererParams & { field: FieldMetadata }) { + const { value, field } = params; + const FieldWidget = getFieldWidget(field.type); + + this.eGui = document.createElement('div'); + this.eGui.className = 'field-widget-cell'; + + if (FieldWidget) { + this.root = createRoot(this.eGui); + this.root.render( + {}} // No-op for read-only mode + field={field} + readonly={true} + /> + ); + } else { + // Fallback to text display + this.eGui.textContent = value != null ? String(value) : ''; + } + } + + getGui() { + return this.eGui; + } + + refresh(params: ICellRendererParams & { field: FieldMetadata }): boolean { + const { value, field } = params; + const FieldWidget = getFieldWidget(field.type); + + if (FieldWidget && this.root) { + this.root.render( + {}} // No-op for read-only mode + field={field} + readonly={true} + /> + ); + return true; + } + + // Fallback to text display when no FieldWidget is available + if (this.eGui) { + this.eGui.textContent = value != null ? String(value) : ''; + return true; + } + + return false; + } + + destroy() { + if (this.root) { + this.root.unmount(); + } + } +} + +/** + * AG Grid Cell Editor using Field Widgets (Edit mode) + */ +export class FieldWidgetCellEditor { + public eGui!: HTMLDivElement; + public root: Root | null = null; + public currentValue: unknown; + public params!: ICellEditorParams & { field: FieldMetadata }; + + init(params: ICellEditorParams & { field: FieldMetadata }) { + this.params = params; + this.currentValue = params.value; + const { field } = params; + const FieldWidget = getFieldWidget(field.type); + + this.eGui = document.createElement('div'); + this.eGui.className = 'field-widget-editor'; + + if (FieldWidget) { + this.root = createRoot(this.eGui); + this.root.render( + { + this.currentValue = newValue; + }} + field={field} + readonly={false} + /> + ); + } else { + // Fallback to input element + const input = document.createElement('input'); + input.value = this.currentValue != null ? String(this.currentValue) : ''; + input.className = 'ag-input-field-input ag-text-field-input'; + input.addEventListener('input', (e) => { + this.currentValue = (e.target as HTMLInputElement).value; + }); + this.eGui.appendChild(input); + setTimeout(() => input.focus(), 0); + } + } + + getGui() { + return this.eGui; + } + + getValue() { + return this.currentValue; + } + + destroy() { + if (this.root) { + this.root.unmount(); + } + } + + isPopup(): boolean { + // Return true for complex widgets that need more space + const popupTypes = ['date', 'datetime', 'select', 'lookup', 'color']; + return popupTypes.includes(this.params.field.type); + } +} + +/** + * Factory function to create cell renderer with field metadata + */ +export function createFieldCellRenderer(field: FieldMetadata) { + return class extends FieldWidgetCellRenderer { + init(params: ICellRendererParams) { + super.init({ ...params, field }); + } + refresh(params: ICellRendererParams): boolean { + return super.refresh({ ...params, field }); + } + }; +} + +/** + * Factory function to create cell editor with field metadata + */ +export function createFieldCellEditor(field: FieldMetadata) { + return class extends FieldWidgetCellEditor { + init(params: ICellEditorParams) { + super.init({ ...params, field }); + } + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df690d7d..c6928ad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -997,6 +997,9 @@ importers: '@object-ui/data-objectstack': specifier: workspace:* version: link:../data-objectstack + '@object-ui/fields': + specifier: workspace:* + version: link:../fields '@object-ui/react': specifier: workspace:* version: link:../react @@ -2949,89 +2952,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -3347,24 +3366,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.1.6': resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} @@ -4186,66 +4209,79 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -4629,24 +4665,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.11': resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.15.11': resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.11': resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.11': resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} @@ -4728,24 +4768,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -5172,41 +5216,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -7840,24 +7892,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}