microui-wc 0.1.0 → 0.1.2
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/AGENTS.md +71 -71
- package/CHANGELOG.md +1 -1
- package/README.md +14 -9
- package/dist/AGENTS.md +116 -116
- package/dist/README.md +21 -16
- package/dist/components.css +1 -1
- package/dist/microui.css +1 -1
- package/dist/microui.esm.js.map +1 -1
- package/dist/microui.min.js.map +1 -1
- package/dist/styles/components/switch.css +1 -1
- package/docs/getting-started.md +3 -3
- package/package.json +38 -10
- package/src/components/mu-schema-form.js +1 -1
- package/src/styles/components/switch.css +7 -8
- package/src/styles/components.css +6 -6
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -33
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -28
- package/.github/workflows/ci.yml +0 -42
- package/.github/workflows/deploy-pages.yml +0 -112
- package/CODE_OF_CONDUCT.md +0 -59
- package/CONTRIBUTING.md +0 -156
- package/SECURITY.md +0 -58
- package/app/.generated/routes/alerts.js +0 -8
- package/app/.generated/routes/avatars.js +0 -8
- package/app/.generated/routes/badges.js +0 -8
- package/app/.generated/routes/buttons.js +0 -10
- package/app/.generated/routes/cards.js +0 -10
- package/app/.generated/routes/checkboxes.js +0 -9
- package/app/.generated/routes/chips.js +0 -8
- package/app/.generated/routes/dropdowns.js +0 -9
- package/app/.generated/routes/home.js +0 -7
- package/app/.generated/routes/icons.js +0 -9
- package/app/.generated/routes/inputs.js +0 -10
- package/app/.generated/routes/installation.js +0 -7
- package/app/.generated/routes/layout.js +0 -9
- package/app/.generated/routes/modals.js +0 -9
- package/app/.generated/routes/navbar.js +0 -7
- package/app/.generated/routes/progress.js +0 -9
- package/app/.generated/routes/radios.js +0 -9
- package/app/.generated/routes/switches.js +0 -9
- package/app/.generated/routes/tabs.js +0 -8
- package/app/.generated/routes/toasts.js +0 -9
- package/app/index.html +0 -67
- package/app/pages/alerts.html +0 -23
- package/app/pages/avatars.html +0 -22
- package/app/pages/badges.html +0 -22
- package/app/pages/buttons.html +0 -71
- package/app/pages/cards.html +0 -54
- package/app/pages/checkboxes.html +0 -39
- package/app/pages/chips.html +0 -23
- package/app/pages/dropdowns.html +0 -41
- package/app/pages/home.html +0 -59
- package/app/pages/icons.html +0 -29
- package/app/pages/inputs.html +0 -66
- package/app/pages/installation.html +0 -34
- package/app/pages/layout.html +0 -30
- package/app/pages/modals.html +0 -21
- package/app/pages/navbar.html +0 -22
- package/app/pages/progress.html +0 -35
- package/app/pages/radios.html +0 -40
- package/app/pages/switches.html +0 -39
- package/app/pages/tabs.html +0 -30
- package/app/pages/toasts.html +0 -22
- package/app-dist/index.html +0 -67
- package/app-dist/pages/alerts.html +0 -23
- package/app-dist/pages/avatars.html +0 -22
- package/app-dist/pages/badges.html +0 -22
- package/app-dist/pages/buttons.html +0 -71
- package/app-dist/pages/cards.html +0 -54
- package/app-dist/pages/checkboxes.html +0 -39
- package/app-dist/pages/chips.html +0 -23
- package/app-dist/pages/dropdowns.html +0 -41
- package/app-dist/pages/home.html +0 -59
- package/app-dist/pages/icons.html +0 -29
- package/app-dist/pages/inputs.html +0 -66
- package/app-dist/pages/installation.html +0 -34
- package/app-dist/pages/layout.html +0 -30
- package/app-dist/pages/modals.html +0 -21
- package/app-dist/pages/navbar.html +0 -22
- package/app-dist/pages/progress.html +0 -35
- package/app-dist/pages/radios.html +0 -40
- package/app-dist/pages/switches.html +0 -39
- package/app-dist/pages/tabs.html +0 -30
- package/app-dist/pages/toasts.html +0 -22
- package/app-dist/pages.json +0 -217
- package/app-dist/routes/alerts.js +0 -5
- package/app-dist/routes/avatars.js +0 -1
- package/app-dist/routes/badges.js +0 -1
- package/app-dist/routes/buttons.js +0 -1
- package/app-dist/routes/cards.js +0 -1
- package/app-dist/routes/checkboxes.js +0 -9
- package/app-dist/routes/chips.js +0 -4
- package/app-dist/routes/chunk-019e5e2f.js +0 -5
- package/app-dist/routes/chunk-0m4j19yd.js +0 -2
- package/app-dist/routes/chunk-0tmmp5q0.js +0 -1
- package/app-dist/routes/chunk-10xn709r.js +0 -1
- package/app-dist/routes/chunk-15m2qcda.js +0 -2
- package/app-dist/routes/chunk-1bh8g23n.js +0 -1
- package/app-dist/routes/chunk-1vg0v937.js +0 -1
- package/app-dist/routes/chunk-1zvcgy3j.js +0 -1
- package/app-dist/routes/chunk-2afb0861.js +0 -1
- package/app-dist/routes/chunk-2c6ttpzt.js +0 -5
- package/app-dist/routes/chunk-3dy30fhs.js +0 -1
- package/app-dist/routes/chunk-426dnces.js +0 -13
- package/app-dist/routes/chunk-44kgxery.js +0 -1
- package/app-dist/routes/chunk-47fdnejd.js +0 -33
- package/app-dist/routes/chunk-49a6t2vq.js +0 -1
- package/app-dist/routes/chunk-4fe1rm5b.js +0 -1
- package/app-dist/routes/chunk-4ggmvkta.js +0 -33
- package/app-dist/routes/chunk-4vkz81q7.js +0 -33
- package/app-dist/routes/chunk-4w4tmj8f.js +0 -31
- package/app-dist/routes/chunk-532s62kr.js +0 -31
- package/app-dist/routes/chunk-5hm3bssy.js +0 -33
- package/app-dist/routes/chunk-5vrh24hc.js +0 -1
- package/app-dist/routes/chunk-61pcg25a.js +0 -1
- package/app-dist/routes/chunk-6nfhygvf.js +0 -1
- package/app-dist/routes/chunk-700e7je6.js +0 -33
- package/app-dist/routes/chunk-7fsn17kg.js +0 -1
- package/app-dist/routes/chunk-7k789b32.js +0 -1
- package/app-dist/routes/chunk-7r46q0ys.js +0 -36
- package/app-dist/routes/chunk-86fmc1fr.js +0 -5
- package/app-dist/routes/chunk-8qth37vw.js +0 -1
- package/app-dist/routes/chunk-924wv8n0.js +0 -1
- package/app-dist/routes/chunk-9mbhgxk9.js +0 -1
- package/app-dist/routes/chunk-a216hyd9.js +0 -1
- package/app-dist/routes/chunk-akzxykh9.js +0 -33
- package/app-dist/routes/chunk-b3dcvy8c.js +0 -1
- package/app-dist/routes/chunk-b74zahz5.js +0 -31
- package/app-dist/routes/chunk-bftj53p2.js +0 -5
- package/app-dist/routes/chunk-c01hnz3e.js +0 -1
- package/app-dist/routes/chunk-d8pvv5km.js +0 -1
- package/app-dist/routes/chunk-dev0aezr.js +0 -2
- package/app-dist/routes/chunk-dh6vnv0e.js +0 -1
- package/app-dist/routes/chunk-dn2cbpva.js +0 -36
- package/app-dist/routes/chunk-dvn0my90.js +0 -1
- package/app-dist/routes/chunk-dvq8mnve.js +0 -36
- package/app-dist/routes/chunk-e8c2gc4d.js +0 -5
- package/app-dist/routes/chunk-ejf9ak2x.js +0 -1
- package/app-dist/routes/chunk-f083m55s.js +0 -1
- package/app-dist/routes/chunk-fnrj28s1.js +0 -31
- package/app-dist/routes/chunk-fvg3yjdp.js +0 -31
- package/app-dist/routes/chunk-g7k381n1.js +0 -1
- package/app-dist/routes/chunk-h01kq2ae.js +0 -13
- package/app-dist/routes/chunk-h4dk761v.js +0 -5
- package/app-dist/routes/chunk-hmx91z2x.js +0 -5
- package/app-dist/routes/chunk-hxbg4m42.js +0 -36
- package/app-dist/routes/chunk-jbjnfp2b.js +0 -2
- package/app-dist/routes/chunk-jxtz5vv6.js +0 -36
- package/app-dist/routes/chunk-jxzcs0ey.js +0 -36
- package/app-dist/routes/chunk-kt7wwhcx.js +0 -1
- package/app-dist/routes/chunk-kzptszyc.js +0 -33
- package/app-dist/routes/chunk-mhgca4w4.js +0 -2
- package/app-dist/routes/chunk-mhswxa20.js +0 -1
- package/app-dist/routes/chunk-n8zfeex6.js +0 -1
- package/app-dist/routes/chunk-pee47b2r.js +0 -1
- package/app-dist/routes/chunk-pesmw829.js +0 -1
- package/app-dist/routes/chunk-pgc4c6f3.js +0 -36
- package/app-dist/routes/chunk-q8egegm1.js +0 -1
- package/app-dist/routes/chunk-q9mn2qyq.js +0 -36
- package/app-dist/routes/chunk-qh0rtaf3.js +0 -5
- package/app-dist/routes/chunk-qqhmk6ye.js +0 -2
- package/app-dist/routes/chunk-qrxygmf7.js +0 -33
- package/app-dist/routes/chunk-r46yzksx.js +0 -36
- package/app-dist/routes/chunk-rgpbw2w0.js +0 -5
- package/app-dist/routes/chunk-rnpzv3d8.js +0 -2
- package/app-dist/routes/chunk-s5v8cv05.js +0 -2
- package/app-dist/routes/chunk-sbwn5bpc.js +0 -1
- package/app-dist/routes/chunk-sqbg8jbt.js +0 -33
- package/app-dist/routes/chunk-sv8dqnf7.js +0 -1
- package/app-dist/routes/chunk-t67sw3za.js +0 -1
- package/app-dist/routes/chunk-tjdpqwdf.js +0 -31
- package/app-dist/routes/chunk-tq2mfghg.js +0 -1
- package/app-dist/routes/chunk-ttn10vt6.js +0 -1
- package/app-dist/routes/chunk-v2hzpjxr.js +0 -1
- package/app-dist/routes/chunk-wfjjkw9y.js +0 -1
- package/app-dist/routes/chunk-wt8cxzmf.js +0 -31
- package/app-dist/routes/chunk-x45d372k.js +0 -5
- package/app-dist/routes/chunk-y3wsazkt.js +0 -1
- package/app-dist/routes/chunk-y7pmgc7t.js +0 -33
- package/app-dist/routes/chunk-zefdt2q3.js +0 -31
- package/app-dist/routes/dropdowns.js +0 -6
- package/app-dist/routes/home.js +0 -1
- package/app-dist/routes/icons.js +0 -1
- package/app-dist/routes/inputs.js +0 -12
- package/app-dist/routes/installation.js +0 -1
- package/app-dist/routes/layout.js +0 -1
- package/app-dist/routes/modals.js +0 -7
- package/app-dist/routes/navbar.js +0 -1
- package/app-dist/routes/progress.js +0 -1
- package/app-dist/routes/radios.js +0 -6
- package/app-dist/routes/switches.js +0 -6
- package/app-dist/routes/tabs.js +0 -1
- package/app-dist/routes/toasts.js +0 -16
- package/assets/fonts/material-symbols-mini.woff2 +0 -0
- package/assets/fonts/material-symbols.woff2 +0 -0
- package/assets/fonts/roboto-400.woff2 +0 -0
- package/assets/fonts/roboto-500.woff2 +0 -0
- package/assets/fonts/roboto-700.woff2 +0 -0
- package/assets/logo-banner-400.jpg +0 -0
- package/assets/logo-banner-400.webp +0 -0
- package/assets/logo-banner-800.webp +0 -0
- package/assets/logo-banner.jpg +0 -0
- package/assets/logo-icon-64.jpg +0 -0
- package/assets/logo-icon-64.webp +0 -0
- package/assets/logo-icon.jpg +0 -0
- package/assets/logo-square.jpg +0 -0
- package/bun.lock +0 -312
- package/bunfig.toml +0 -4
- package/custom-elements.json +0 -1916
- package/demo/api/sample-data.json +0 -38
- package/demo/content/alerts.html +0 -115
- package/demo/content/avatars.html +0 -70
- package/demo/content/badges.html +0 -65
- package/demo/content/buttons.html +0 -188
- package/demo/content/callouts.html +0 -91
- package/demo/content/cards.html +0 -121
- package/demo/content/checkboxes.html +0 -178
- package/demo/content/chips.html +0 -67
- package/demo/content/codeblocks.html +0 -101
- package/demo/content/confirms.html +0 -115
- package/demo/content/datatables.html +0 -149
- package/demo/content/dividers.html +0 -119
- package/demo/content/dropdowns.html +0 -89
- package/demo/content/enterprise.html +0 -252
- package/demo/content/home.html +0 -149
- package/demo/content/icons.html +0 -89
- package/demo/content/inputs.html +0 -135
- package/demo/content/installation.html +0 -16
- package/demo/content/layout.html +0 -136
- package/demo/content/modals.html +0 -141
- package/demo/content/navbar.html +0 -70
- package/demo/content/progress.html +0 -119
- package/demo/content/radios.html +0 -88
- package/demo/content/skeletons.html +0 -109
- package/demo/content/spinners.html +0 -96
- package/demo/content/switches.html +0 -84
- package/demo/content/tables.html +0 -124
- package/demo/content/tabs.html +0 -85
- package/demo/content/toasts.html +0 -116
- package/demo/content/tooltips.html +0 -107
- package/demo/content/virtual-lists.html +0 -233
- package/demo/favicon.ico +0 -0
- package/demo/favicon.png +0 -0
- package/demo/full.html +0 -52
- package/demo/iife.html +0 -46
- package/demo/manifest.json +0 -34
- package/demo/pages/datatable-demo.html +0 -237
- package/demo/pages/prompt-ui-demo.html +0 -218
- package/demo/pages/responsive-demo.html +0 -122
- package/demo/pages/schema-form-demo.html +0 -270
- package/demo/robots.txt +0 -6
- package/demo/shell.html +0 -712
- package/demo/sw.js +0 -387
- package/lighthouse-audit.mjs +0 -113
- package/scripts/analyze-components.js +0 -105
- package/scripts/build-app.js +0 -193
- package/scripts/build-framework.js +0 -444
- package/scripts/build-utils.js +0 -101
- package/scripts/test-isolated.js +0 -151
- package/server.js +0 -256
- package/tests/agents/agent-integration.test.js +0 -76
- package/tests/benchmark.html +0 -296
- package/tests/build/scan-components.test.js +0 -173
- package/tests/components/all-components.test.js +0 -245
- package/tests/components/all-missing-components.test.js +0 -574
- package/tests/components/mu-alert.test.js +0 -113
- package/tests/components/mu-avatar.test.js +0 -148
- package/tests/components/mu-badge.test.js +0 -92
- package/tests/components/mu-button.test.js +0 -112
- package/tests/components/mu-card.test.js +0 -89
- package/tests/components/mu-checkbox.test.js +0 -158
- package/tests/components/mu-chip.test.js +0 -118
- package/tests/components/mu-container.test.js +0 -120
- package/tests/components/mu-divider.test.js +0 -98
- package/tests/components/mu-drawer-item.test.js +0 -199
- package/tests/components/mu-drawer.test.js +0 -96
- package/tests/components/mu-dropdown.test.js +0 -125
- package/tests/components/mu-form.test.js +0 -138
- package/tests/components/mu-grid.test.js +0 -135
- package/tests/components/mu-icon.test.js +0 -110
- package/tests/components/mu-input.test.js +0 -131
- package/tests/components/mu-lazy.test.js +0 -103
- package/tests/components/mu-modal.test.js +0 -275
- package/tests/components/mu-navbar.test.js +0 -101
- package/tests/components/mu-progress.test.js +0 -115
- package/tests/components/mu-radio.test.js +0 -114
- package/tests/components/mu-repeat.test.js +0 -106
- package/tests/components/mu-sidebar.test.js +0 -126
- package/tests/components/mu-skeleton.test.js +0 -162
- package/tests/components/mu-stack.test.js +0 -143
- package/tests/components/mu-switch.test.js +0 -292
- package/tests/components/mu-table.test.js +0 -124
- package/tests/components/mu-tabs.test.js +0 -104
- package/tests/components/mu-textarea.test.js +0 -115
- package/tests/components/mu-toast.test.js +0 -321
- package/tests/components/mu-tooltip.test.js +0 -133
- package/tests/components/mu-virtual-list.test.js +0 -109
- package/tests/core/MuElement.test.js +0 -120
- package/tests/core/agent-api.test.js +0 -125
- package/tests/core/all-core-modules.test.js +0 -442
- package/tests/core/bus.test.js +0 -364
- package/tests/core/component-schema.test.js +0 -160
- package/tests/core/feature-registry.test.js +0 -198
- package/tests/core/form-state.test.js +0 -167
- package/tests/core/http.test.js +0 -119
- package/tests/core/keyboard.test.js +0 -319
- package/tests/core/layers.test.js +0 -129
- package/tests/core/namespaced-stores.test.js +0 -114
- package/tests/core/render.test.js +0 -121
- package/tests/core/ripple.test.js +0 -131
- package/tests/core/router.test.js +0 -89
- package/tests/core/scheduler.test.js +0 -121
- package/tests/core/signals.test.js +0 -128
- package/tests/core/store.test.js +0 -171
- package/tests/core/transitions.test.js +0 -82
- package/tests/e2e/accessibility-harness.html +0 -58
- package/tests/e2e/accessibility.test.js +0 -401
- package/tests/e2e/agent-features.test.js +0 -372
- package/tests/e2e/card-spacing.test.js +0 -287
- package/tests/e2e/components.test.js +0 -439
- package/tests/e2e/demo-routes.test.js +0 -478
- package/tests/e2e/layout-css-fallback.test.js +0 -334
- package/tests/e2e/mu-alert.e2e.test.js +0 -111
- package/tests/e2e/mu-checkbox.test.js +0 -489
- package/tests/e2e/mu-chip.test.js +0 -347
- package/tests/e2e/mu-form.test.js +0 -499
- package/tests/e2e/mu-icon.test.js +0 -114
- package/tests/e2e/mu-radio.test.js +0 -113
- package/tests/e2e/mu-skeleton.test.js +0 -140
- package/tests/e2e/mu-switch.test.js +0 -415
- package/tests/e2e/mu-tabs.test.js +0 -494
- package/tests/e2e/mu-textarea.test.js +0 -242
- package/tests/e2e/mu-virtual-list.test.js +0 -427
- package/tests/e2e/perf-memory.test.js +0 -161
- package/tests/e2e/puppeteer-helper.js +0 -137
- package/tests/e2e/puppeteer.test.js +0 -226
- package/tests/e2e/pwa.test.js +0 -261
- package/tests/e2e/test-harness.html +0 -319
- package/tests/manual/test-components.html +0 -120
- package/tests/memory-test.html +0 -309
- package/tests/setup-dom.js +0 -93
- package/tests/visual-test.html +0 -301
package/dist/microui.esm.js.map
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"/**\n * @fileoverview mu-repeat - Efficient List Rendering with Keyed Diffing\n * \n * Renders arrays efficiently by only updating changed items.\n * Uses key-based reconciliation like React/Vue.\n * \n * Usage:\n * <mu-repeat id=\"list\"></mu-repeat>\n * \n * const list = document.getElementById('list');\n * list.items = users;\n * list.keyFn = (user) => user.id;\n * list.renderItem = (user) => `<div>${user.name}</div>`;\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { scheduler } from '../core/render.js';\n\nexport class MuRepeat extends MuElement {\n static baseClass = 'mu-repeat';\n static observedAttributes = [];\n\n #items = [];\n #keyFn = (item, index) => index;\n #renderItem = (item) => `<div>${JSON.stringify(item)}</div>`;\n #itemNodes = new Map(); // key -> DOM element\n\n set items(arr) {\n const oldItems = this.#items;\n this.#items = arr || [];\n\n // Schedule batched update\n scheduler.schedule(() => this.#reconcile(oldItems));\n }\n\n get items() {\n return this.#items;\n }\n\n set keyFn(fn) {\n this.#keyFn = fn;\n }\n\n set renderItem(fn) {\n this.#renderItem = fn;\n }\n\n render() {\n this.style.display = 'contents';\n }\n\n /**\n * Efficient reconciliation - only updates changed items\n */\n #reconcile(oldItems) {\n const newKeys = new Set();\n const fragment = document.createDocumentFragment();\n\n // Build set of new keys\n for (let i = 0; i < this.#items.length; i++) {\n const key = this.#keyFn(this.#items[i], i);\n newKeys.add(key);\n }\n\n // Remove items that no longer exist\n for (const [key, node] of this.#itemNodes) {\n if (!newKeys.has(key)) {\n node.remove();\n this.#itemNodes.delete(key);\n }\n }\n\n // Add/update items\n let prevNode = null;\n\n for (let i = 0; i < this.#items.length; i++) {\n const item = this.#items[i];\n const key = this.#keyFn(item, i);\n\n let node = this.#itemNodes.get(key);\n\n if (!node) {\n // Create new node\n const wrapper = document.createElement('div');\n wrapper.innerHTML = this.#renderItem(item, i);\n node = wrapper.firstElementChild || wrapper;\n node.dataset.muKey = key;\n this.#itemNodes.set(key, node);\n } else {\n // Update existing node content\n const newContent = this.#renderItem(item, i);\n if (node.outerHTML !== newContent) {\n const wrapper = document.createElement('div');\n wrapper.innerHTML = newContent;\n const newNode = wrapper.firstElementChild || wrapper;\n newNode.dataset.muKey = key;\n node.replaceWith(newNode);\n this.#itemNodes.set(key, newNode);\n node = newNode;\n }\n }\n\n // Position correctly\n if (prevNode) {\n if (node.previousElementSibling !== prevNode) {\n prevNode.after(node);\n }\n } else {\n if (this.firstElementChild !== node) {\n this.prepend(node);\n }\n }\n\n prevNode = node;\n }\n }\n\n /**\n * Get DOM element for a key\n */\n getElement(key) {\n return this.#itemNodes.get(key);\n }\n\n /**\n * Force full re-render\n */\n refresh() {\n this.#itemNodes.clear();\n this.innerHTML = '';\n scheduler.schedule(() => this.#reconcile([]));\n }\n}\n\ndefine('mu-repeat', MuRepeat);\n",
|
|
65
65
|
"/**\n * @fileoverview mu-fetch - Declarative Data Fetcher Component\n * Agent-friendly component for fetching data with automatic loading/error states\n * \n * @example\n * <mu-fetch url=\"/api/users\" id=\"users\">\n * <template slot=\"loading\"><mu-skeleton></mu-skeleton></template>\n * <template slot=\"error\"><mu-alert variant=\"error\">${error}</mu-alert></template>\n * </mu-fetch>\n * \n * // Access data\n * const fetcher = document.getElementById('users');\n * fetcher.data; // The fetched data\n * fetcher.refetch(); // Re-fetch data\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\n\n/**\n * @typedef {'idle'|'loading'|'success'|'error'} FetchState\n */\n\n/**\n * @typedef {Object} FetchResult\n * @property {FetchState} state\n * @property {any} data\n * @property {Error|null} error\n */\n\nexport class MuFetch extends MuElement {\n static baseClass = 'mu-fetch';\n static observedAttributes = ['url', 'method', 'auto', 'interval'];\n\n /** @type {FetchState} */\n state = 'idle';\n\n /** @type {any} */\n data = null;\n\n /** @type {Error|null} */\n error = null;\n\n /** @type {AbortController|null} */\n _controller = null;\n\n /** @type {number|null} */\n _intervalId = null;\n\n connectedCallback() {\n // Store templates before render\n this._loadingTemplate = this.querySelector('template[slot=\"loading\"]')?.innerHTML ||\n '<mu-spinner></mu-spinner>';\n this._errorTemplate = this.querySelector('template[slot=\"error\"]')?.innerHTML ||\n '<mu-alert variant=\"error\">Error loading data</mu-alert>';\n this._emptyTemplate = this.querySelector('template[slot=\"empty\"]')?.innerHTML ||\n '<p>No data</p>';\n\n super.connectedCallback();\n\n // Auto-fetch on connect\n if (this.has('auto') || this.hasAttribute('url')) {\n this.fetch();\n }\n\n // Setup interval refresh\n const interval = parseInt(this.attr('interval', '0'));\n if (interval > 0) {\n this.setInterval(() => this.fetch(), interval);\n }\n }\n\n render() {\n // Render based on current state\n this.innerHTML = ''; // Clear content\n\n switch (this.state) {\n case 'loading':\n // Safe: _loadingTemplate is trusted or empty, but let's wrap it container\n const loadingContainer = document.createElement('div');\n loadingContainer.className = 'mu-fetch__loading';\n loadingContainer.innerHTML = this._loadingTemplate; // _loadingTemplate comes from initial slot content which is trusted\n this.appendChild(loadingContainer);\n break;\n case 'error':\n const errorContainer = document.createElement('div');\n errorContainer.className = 'mu-fetch__error';\n\n // Safe: Create error message safely using textContent\n // We don't use the template string replacement anymore to avoid XSS\n // If user provided a custom error template, we parse it safely or just use default\n\n const errorMessage = this.error?.message || 'Unknown error';\n\n if (this._errorTemplate && this._errorTemplate.includes('${error}')) {\n // If template is complex, we might need a safer way, but for now \n // let's stick to safe text replacement if possible or just use textContent\n // Actually, simply setting textContent into the container is safest for the message\n errorContainer.textContent = `Error: ${errorMessage}`;\n // If we want to respect the slot, we'd need a safe interpolate, \n // but for high, security we break the unsafe template behavior for errors.\n } else {\n const alert = document.createElement('mu-alert');\n alert.setAttribute('variant', 'error');\n alert.textContent = errorMessage; // Safe\n errorContainer.appendChild(alert);\n }\n\n this.appendChild(errorContainer);\n break;\n case 'success':\n if (!this.data || (Array.isArray(this.data) && this.data.length === 0)) {\n const emptyContainer = document.createElement('div');\n emptyContainer.className = 'mu-fetch__empty';\n emptyContainer.innerHTML = this._emptyTemplate; // Trusted slot content\n this.appendChild(emptyContainer);\n } else {\n const dataContainer = document.createElement('div');\n dataContainer.className = 'mu-fetch__data';\n\n // Dataslot or renderItem function\n if (this.renderItem && Array.isArray(this.data)) {\n // WARNING: renderItem expects to return a string currently. \n // To be safe, we should check if it returns a Node or string.\n // For now we will assume string but we can sanitize it? \n // Actually, following the plan, we change default behavior but if user provides function\n // they might output HTML. \n // Let's implement safe node appending if possible.\n this.data.forEach(item => {\n const result = this.renderItem(item);\n if (result instanceof Node) {\n dataContainer.appendChild(result);\n } else {\n // If string, we must be careful. \n // Ideally we use insertAdjacentHTML but that is XSS vector if renderItem is unsafe.\n // We will document that renderItem must be safe.\n // For the PoC, we will show that we can make it safe by NOT using innerHTML on the container directly\n // but appending item by item?\n // innerHTML += result is bad.\n const temp = document.createElement('div');\n temp.innerHTML = result; // Still unsafe if result is bad.\n // We can't fix user's renderItem but we can fix the container\n dataContainer.insertAdjacentHTML('beforeend', result);\n }\n });\n } else {\n const slot = document.createElement('slot');\n slot.name = 'data';\n dataContainer.appendChild(slot);\n this.emit('mu-data', { data: this.data });\n }\n this.appendChild(dataContainer);\n }\n break;\n }\n }\n\n /**\n * Fetch data from the URL\n * @param {RequestInit} [options] - Fetch options override\n * @returns {Promise<any>}\n */\n async fetch(options = {}) {\n const url = this.attr('url', '');\n if (!url) {\n console.warn('mu-fetch: No URL provided');\n return null;\n }\n\n // Abort any in-flight request\n if (this._controller) {\n this._controller.abort();\n }\n this._controller = new AbortController();\n\n this.state = 'loading';\n this.error = null;\n this.render();\n this.emit('mu-loading');\n\n try {\n const method = this.attr('method', 'GET');\n const response = await fetch(url, {\n method,\n signal: this._controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n ...options.headers\n },\n ...options\n });\n\n if (!response.ok) {\n // If the server returns JSON error, try to parse it\n let errorMsg = `HTTP ${response.status}: ${response.statusText}`;\n try {\n const errorJson = await response.json();\n if (errorJson.message) errorMsg = errorJson.message;\n } catch (e) {\n // Ignore json parse error\n }\n throw new Error(errorMsg);\n }\n\n this.data = await response.json();\n this.state = 'success';\n this.render();\n this.emit('mu-success', { data: this.data });\n return this.data;\n } catch (err) {\n if (err.name === 'AbortError') {\n return null; // Aborted, ignore\n }\n this.error = /** @type {Error} */ (err);\n this.state = 'error';\n this.render();\n this.emit('mu-error', { error: this.error });\n return null;\n }\n }\n\n /**\n * Refetch the current URL\n * @returns {Promise<any>}\n */\n refetch() {\n return this.fetch();\n }\n\n /**\n * Set custom render function for array data\n * @param {(item: any, index: number) => string} fn\n */\n set renderItem(fn) {\n this._renderItem = fn;\n }\n\n get renderItem() {\n return this._renderItem;\n }\n\n update(attr, newValue, oldValue) {\n if (attr === 'url' && newValue !== oldValue) {\n this.fetch();\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this._controller) {\n this._controller.abort();\n }\n // Note: setInterval cleanup now handled by MuElement via super.disconnectedCallback()\n }\n}\n\ndefine('mu-fetch', MuFetch);\n",
|
|
66
66
|
"/**\n * mu-datatable.js - Smart Data Table Component\n * \n * AI-First data table with built-in sorting, pagination, and filtering.\n * Designed to minimize boilerplate code when displaying tabular data.\n * \n * @example\n * <mu-datatable\n * columns='[\n * {\"field\": \"name\", \"label\": \"Name\", \"sortable\": true},\n * {\"field\": \"email\", \"label\": \"Email\"},\n * {\"field\": \"role\", \"label\": \"Role\", \"filterable\": true}\n * ]'\n * page-size=\"10\"\n * sortable>\n * </mu-datatable>\n * \n * <script>\n * document.querySelector('mu-datatable').setData(usersArray);\n * </script>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\nimport { debounce } from '../core/render.js';\n\nexport class MuDataTable extends MuElement {\n static get observedAttributes() {\n return ['columns', 'page-size', 'sortable', 'selectable', 'filterable', 'empty-message'];\n }\n\n static baseClass = 'mu-datatable';\n static cssFile = 'datatable';\n\n constructor() {\n super();\n\n // Internal state\n this._data = [];\n this._filteredData = [];\n this._columns = [];\n this._currentPage = 1;\n this._sortField = null;\n this._sortDirection = 'asc';\n this._selectedRows = new Set();\n this._filterValue = '';\n }\n\n // === PROPERTIES ===\n\n get columns() {\n const attr = this.getAttribute('columns');\n if (attr) {\n try {\n return JSON.parse(attr);\n } catch (e) {\n console.warn('[mu-datatable] Invalid columns JSON:', e);\n return [];\n }\n }\n return this._columns;\n }\n\n set columns(value) {\n this._columns = Array.isArray(value) ? value : [];\n if (this.isConnected) this.render();\n }\n\n get pageSize() {\n return parseInt(this.getAttribute('page-size')) || 10;\n }\n\n set pageSize(value) {\n this.setAttribute('page-size', value);\n }\n\n get sortable() {\n return this.hasAttribute('sortable');\n }\n\n get selectable() {\n return this.hasAttribute('selectable');\n }\n\n get filterable() {\n return this.hasAttribute('filterable');\n }\n\n get emptyMessage() {\n return this.getAttribute('empty-message') || 'No data available';\n }\n\n // === LIFECYCLE ===\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n // === PUBLIC API ===\n\n /**\n * Set table data programmatically\n * @param {Array<Object>} data - Array of row objects\n */\n setData(data) {\n this._data = Array.isArray(data) ? data : [];\n this._filteredData = [...this._data];\n this._currentPage = 1;\n this._selectedRows.clear();\n this._applySort();\n this.render();\n\n this.emit('mu-data-change', { data: this._data, count: this._data.length }, { bubbles: true });\n }\n\n /**\n * Get current data\n * @returns {Array<Object>}\n */\n getData() {\n return [...this._data];\n }\n\n /**\n * Get current sort state\n * @returns {{ field: string|null, direction: 'asc'|'desc' }}\n */\n getSortState() {\n return { field: this._sortField, direction: this._sortDirection };\n }\n\n /**\n * Get selected rows\n * @returns {Array<Object>}\n */\n getSelectedRows() {\n return this._data.filter((_, idx) => this._selectedRows.has(idx));\n }\n\n /**\n * Get current page info\n * @returns {{ page: number, pageSize: number, totalPages: number, totalRows: number }}\n */\n getPageInfo() {\n const totalPages = Math.ceil(this._filteredData.length / this.pageSize);\n return {\n page: this._currentPage,\n pageSize: this.pageSize,\n totalPages,\n totalRows: this._filteredData.length\n };\n }\n\n /**\n * Go to specific page\n * @param {number} page\n */\n goToPage(page) {\n const maxPage = Math.ceil(this._filteredData.length / this.pageSize) || 1;\n this._currentPage = Math.max(1, Math.min(page, maxPage));\n this.render();\n\n this.emit('mu-page-change', this.getPageInfo(), { bubbles: true });\n }\n\n /**\n * Sort by field\n * @param {string} field\n * @param {'asc'|'desc'} [direction]\n */\n sortBy(field, direction) {\n if (this._sortField === field && !direction) {\n this._sortDirection = this._sortDirection === 'asc' ? 'desc' : 'asc';\n } else {\n this._sortField = field;\n this._sortDirection = direction || 'asc';\n }\n this._applySort();\n this.render();\n\n this.emit('mu-sort-change', this.getSortState(), { bubbles: true });\n }\n\n /**\n * Filter data\n * @param {string} query\n */\n filter(query) {\n this._filterValue = query.toLowerCase();\n this._applyFilter();\n this._currentPage = 1;\n this.render(false); // Partial render - preserve search input focus\n }\n\n // === INTERNAL METHODS ===\n\n _applySort() {\n if (!this._sortField) return;\n\n this._filteredData.sort((a, b) => {\n const valA = a[this._sortField] ?? '';\n const valB = b[this._sortField] ?? '';\n\n // Handle different types\n if (typeof valA === 'number' && typeof valB === 'number') {\n return this._sortDirection === 'asc' ? valA - valB : valB - valA;\n }\n\n const strA = String(valA).toLowerCase();\n const strB = String(valB).toLowerCase();\n const result = strA.localeCompare(strB);\n return this._sortDirection === 'asc' ? result : -result;\n });\n }\n\n _applyFilter() {\n if (!this._filterValue) {\n this._filteredData = [...this._data];\n return;\n }\n\n const filterableColumns = this.columns.filter(c => c.filterable !== false);\n const fields = filterableColumns.length > 0\n ? filterableColumns.map(c => c.field)\n : this.columns.map(c => c.field);\n\n this._filteredData = this._data.filter(row => {\n return fields.some(field => {\n const value = String(row[field] ?? '').toLowerCase();\n return value.includes(this._filterValue);\n });\n });\n }\n\n _getPageData() {\n const start = (this._currentPage - 1) * this.pageSize;\n const end = start + this.pageSize;\n return this._filteredData.slice(start, end);\n }\n\n _handleHeaderClick(field) {\n const column = this.columns.find(c => c.field === field);\n if (column?.sortable !== false && this.sortable) {\n this.sortBy(field);\n }\n }\n\n _handleRowSelect(index, checked) {\n const actualIndex = (this._currentPage - 1) * this.pageSize + index;\n if (checked) {\n this._selectedRows.add(actualIndex);\n } else {\n this._selectedRows.delete(actualIndex);\n }\n this.render();\n\n this.emit('mu-selection-change', { selected: this.getSelectedRows() }, { bubbles: true });\n }\n\n _handleSelectAll(checked) {\n const pageData = this._getPageData();\n const startIdx = (this._currentPage - 1) * this.pageSize;\n\n pageData.forEach((_, idx) => {\n if (checked) {\n this._selectedRows.add(startIdx + idx);\n } else {\n this._selectedRows.delete(startIdx + idx);\n }\n });\n\n this.render();\n\n this.emit('mu-selection-change', { selected: this.getSelectedRows() }, { bubbles: true });\n }\n\n render(fullRender = true) {\n const columns = this.columns;\n const pageData = this._getPageData();\n const { page, totalPages, totalRows } = this.getPageInfo();\n const startIdx = (this._currentPage - 1) * this.pageSize;\n\n // Check if all current page items are selected\n const allSelected = pageData.length > 0 &&\n pageData.every((_, idx) => this._selectedRows.has(startIdx + idx));\n\n // Surgical update: only update tbody and info if already rendered\n const existingTbody = this.querySelector('.mu-datatable-table tbody');\n const existingInfo = this.querySelector('.mu-datatable-info');\n const existingPagination = this.querySelector('.mu-datatable-pagination');\n\n if (!fullRender && existingTbody) {\n // Update only the parts that changed\n existingTbody.innerHTML = this._renderTbody(pageData, columns, startIdx, allSelected);\n\n if (existingInfo) {\n existingInfo.innerHTML = `${totalRows} row${totalRows !== 1 ? 's' : ''}${this._selectedRows.size > 0 ? ` · ${this._selectedRows.size} selected` : ''}`;\n }\n\n if (existingPagination) {\n existingPagination.innerHTML = this._renderPaginationContent(page, totalPages, totalRows);\n } else if (totalPages > 1) {\n // Need to add pagination\n const wrapper = this.querySelector('.mu-datatable-wrapper');\n if (wrapper) {\n wrapper.insertAdjacentHTML('beforeend', this._renderPagination(page, totalPages, totalRows));\n }\n }\n\n this._attachEventListeners();\n return;\n }\n\n // Full render\n this.innerHTML = `\n <div class=\"mu-datatable-wrapper\">\n ${this.filterable ? `\n <div class=\"mu-datatable-toolbar\">\n <div class=\"mu-datatable-search\">\n <input \n type=\"search\" \n placeholder=\"Search...\" \n value=\"${escapeHTML(this._filterValue)}\"\n >\n </div>\n <div class=\"mu-datatable-info\">\n ${totalRows} row${totalRows !== 1 ? 's' : ''}\n ${this._selectedRows.size > 0 ? ` · ${this._selectedRows.size} selected` : ''}\n </div>\n </div>\n ` : ''}\n\n <table class=\"mu-datatable-table\">\n <thead>\n <tr>\n ${this.selectable ? `\n <th class=\"mu-datatable-checkbox-cell\">\n <input \n type=\"checkbox\" \n ${allSelected ? 'checked' : ''}\n data-select-all\n >\n </th>\n ` : ''}\n ${columns.map(col => {\n const isSortable = col.sortable !== false && this.sortable;\n const isSorted = this._sortField === col.field;\n const sortIcon = isSorted\n ? (this._sortDirection === 'asc' ? '↑' : '↓')\n : '↕';\n return `\n <th \n class=\"${isSortable ? 'mu-datatable-sortable' : ''} ${isSorted ? 'mu-datatable-sorted' : ''}\"\n data-field=\"${escapeHTML(col.field)}\"\n >\n ${escapeHTML(col.label || col.field)}\n ${isSortable ? `<span class=\"mu-datatable-sort-icon\">${sortIcon}</span>` : ''}\n </th>\n `;\n }).join('')}\n </tr>\n </thead>\n <tbody>\n ${this._renderTbody(pageData, columns, startIdx, allSelected)}\n </tbody>\n </table>\n\n ${this._renderPagination(page, totalPages, totalRows)}\n </div>\n `;\n\n this._attachEventListeners();\n }\n\n _renderTbody(pageData, columns, startIdx, allSelected) {\n if (pageData.length === 0) {\n return `\n <tr>\n <td colspan=\"${columns.length + (this.selectable ? 1 : 0)}\" class=\"mu-datatable-empty-state\">\n ${escapeHTML(this.emptyMessage)}\n </td>\n </tr>\n `;\n }\n\n return pageData.map((row, idx) => {\n const actualIdx = startIdx + idx;\n const isSelected = this._selectedRows.has(actualIdx);\n\n return `\n <tr class=\"${isSelected ? 'mu-datatable-selected' : ''}\" data-row-index=\"${idx}\">\n ${this.selectable ? `\n <td class=\"mu-datatable-checkbox-cell\">\n <input \n type=\"checkbox\" \n ${isSelected ? 'checked' : ''}\n data-row-select=\"${idx}\"\n >\n </td>\n ` : ''}\n ${columns.map(col => `\n <td data-field=\"${escapeHTML(col.field)}\">\n ${col.render ? col.render(row[col.field], row) : escapeHTML(row[col.field] ?? '')}\n </td>\n `).join('')}\n </tr>\n `;\n }).join('');\n }\n\n _renderPagination(page, totalPages, totalRows) {\n if (totalPages <= 1) return '';\n return `\n <div class=\"mu-datatable-pagination\">\n ${this._renderPaginationContent(page, totalPages, totalRows)}\n </div>\n `;\n }\n\n _renderPaginationContent(page, totalPages, totalRows) {\n return `\n <div class=\"mu-datatable-pagination-info\">\n Showing ${(page - 1) * this.pageSize + 1}–${Math.min(page * this.pageSize, totalRows)} of ${totalRows}\n </div>\n <div class=\"mu-datatable-pagination-controls\">\n <button class=\"mu-datatable-pagination-btn\" data-page=\"prev\" ${page <= 1 ? 'disabled' : ''}>\n ←\n </button>\n ${this._getPaginationButtons(page, totalPages)}\n <button class=\"mu-datatable-pagination-btn\" data-page=\"next\" ${page >= totalPages ? 'disabled' : ''}>\n →\n </button>\n </div>\n `;\n }\n\n _getPaginationButtons(current, total) {\n const buttons = [];\n const maxButtons = 5;\n\n let start = Math.max(1, current - Math.floor(maxButtons / 2));\n let end = Math.min(total, start + maxButtons - 1);\n\n if (end - start < maxButtons - 1) {\n start = Math.max(1, end - maxButtons + 1);\n }\n\n for (let i = start; i <= end; i++) {\n buttons.push(`\n <button \n class=\"mu-datatable-pagination-btn ${i === current ? 'mu-datatable-pagination-btn-active' : ''}\"\n data-page=\"${i}\"\n >\n ${i}\n </button>\n `);\n }\n\n return buttons.join('');\n }\n\n _attachEventListeners() {\n // Header click for sorting\n this.querySelectorAll('th.mu-datatable-sortable').forEach(th => {\n this.listen(th, 'click', () => {\n this._handleHeaderClick(th.dataset.field);\n });\n });\n\n // Pagination\n this.querySelectorAll('.mu-datatable-pagination-btn[data-page]').forEach(btn => {\n this.listen(btn, 'click', () => {\n const page = btn.dataset.page;\n if (page === 'prev') {\n this.goToPage(this._currentPage - 1);\n } else if (page === 'next') {\n this.goToPage(this._currentPage + 1);\n } else {\n this.goToPage(parseInt(page));\n }\n });\n });\n\n // Row selection\n this.querySelectorAll('[data-row-select]').forEach(cb => {\n this.listen(cb, 'change', (e) => {\n this._handleRowSelect(parseInt(cb.dataset.rowSelect), e.target.checked);\n });\n });\n\n // Select all\n const selectAll = this.querySelector('[data-select-all]');\n if (selectAll) {\n this.listen(selectAll, 'change', (e) => {\n this._handleSelectAll(e.target.checked);\n });\n }\n\n // Search/filter\n const searchInput = this.querySelector('.mu-datatable-search input');\n if (searchInput) {\n const debouncedFilter = debounce((value) => this.filter(value), 300);\n this.listen(searchInput, 'input', (e) => debouncedFilter(e.target.value));\n }\n }\n}\n\nif (!customElements.get('mu-datatable')) {\n customElements.define('mu-datatable', MuDataTable);\n}\n",
|
|
67
|
-
"/**\n * mu-schema-form.js - Auto-Generated Forms from JSON Schema\n * \n * AI-First form component that generates microUI form fields automatically\n * from a JSON Schema definition. Dramatically reduces boilerplate for form creation.\n * \n * @example\n * <mu-schema-form id=\"user-form\"></mu-schema-form>\n * \n * <script>\n * const form = document.getElementById('user-form');\n * form.schema = {\n * type: 'object',\n * properties: {\n * name: { type: 'string', title: 'Full Name', minLength: 2 },\n * email: { type: 'string', format: 'email', title: 'Email' },\n * role: { type: 'string', enum: ['user', 'admin'], title: 'Role' },\n * active: { type: 'boolean', title: 'Active', default: true }\n * },\n * required: ['name', 'email']\n * };\n * \n * form.addEventListener('mu-submit', (e) => {\n * console.log('Form data:', e.detail);\n * });\n * </script>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\n\nexport class MuSchemaForm extends MuElement {\n static get observedAttributes() {\n return ['submit-label', 'reset-label', 'inline', 'readonly', 'disabled'];\n }\n\n static baseClass = 'mu-schema-form';\n static cssFile = 'schema-form';\n\n constructor() {\n super();\n\n this._schema = null;\n this._values = {};\n this._errors = {};\n this._touched = {};\n }\n\n // === PROPERTIES ===\n\n get schema() {\n return this._schema;\n }\n\n set schema(value) {\n this._schema = value;\n this._initializeValues();\n if (this.isConnected) this.render();\n }\n\n get submitLabel() {\n return this.getAttribute('submit-label') || 'Submit';\n }\n\n get resetLabel() {\n return this.getAttribute('reset-label') || 'Reset';\n }\n\n get inline() {\n return this.hasAttribute('inline');\n }\n\n get readonly() {\n return this.hasAttribute('readonly');\n }\n\n get disabled() {\n return this.hasAttribute('disabled');\n }\n\n // === LIFECYCLE ===\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n // === PUBLIC API ===\n\n /**\n * Get current form values\n * @returns {Object}\n */\n getValues() {\n return { ...this._values };\n }\n\n /**\n * Set form values\n * @param {Object} values\n */\n setValues(values) {\n this._values = { ...this._values, ...values };\n this.render();\n }\n\n /**\n * Validate all fields\n * @returns {boolean} True if valid\n */\n validate() {\n if (!this._schema || !this._schema.properties) return true;\n\n this._errors = {};\n const required = this._schema.required || [];\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n const value = this._values[field];\n const errors = this._validateField(field, value, def, required.includes(field));\n if (errors.length > 0) {\n this._errors[field] = errors;\n }\n }\n\n this.render();\n return Object.keys(this._errors).length === 0;\n }\n\n /**\n * Get validation errors\n * @returns {Object}\n */\n getErrors() {\n return { ...this._errors };\n }\n\n /**\n * Reset form to default values\n */\n reset() {\n this._initializeValues();\n this._errors = {};\n this._touched = {};\n this.render();\n\n this.emit('mu-reset', { values: this.getValues() }, { bubbles: true });\n }\n\n /**\n * Submit the form programmatically\n */\n submit() {\n if (this.validate()) {\n this.emit('mu-submit', this.getValues(), { bubbles: true });\n }\n }\n\n // === INTERNAL METHODS ===\n\n _initializeValues() {\n this._values = {};\n if (!this._schema || !this._schema.properties) return;\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n if (def.default !== undefined) {\n this._values[field] = def.default;\n } else if (def.type === 'boolean') {\n this._values[field] = false;\n } else if (def.type === 'number' || def.type === 'integer') {\n this._values[field] = null;\n } else {\n this._values[field] = '';\n }\n }\n }\n\n _validateField(field, value, def, isRequired) {\n const errors = [];\n\n // Required check\n if (isRequired && (value === '' || value === null || value === undefined)) {\n errors.push(`${def.title || field} is required`);\n return errors;\n }\n\n if (value === '' || value === null || value === undefined) return errors;\n\n // Type-specific validation\n if (def.type === 'string') {\n if (def.minLength && value.length < def.minLength) {\n errors.push(`Minimum ${def.minLength} characters`);\n }\n if (def.maxLength && value.length > def.maxLength) {\n errors.push(`Maximum ${def.maxLength} characters`);\n }\n if (def.pattern && !new RegExp(def.pattern).test(value)) {\n errors.push(def.patternError || 'Invalid format');\n }\n if (def.format === 'email' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push('Invalid email address');\n }\n if (def.format === 'url' && !/^https?:\\/\\/.+/.test(value)) {\n errors.push('Invalid URL');\n }\n }\n\n if (def.type === 'number' || def.type === 'integer') {\n const num = Number(value);\n if (isNaN(num)) {\n errors.push('Must be a number');\n } else {\n if (def.minimum !== undefined && num < def.minimum) {\n errors.push(`Minimum value is ${def.minimum}`);\n }\n if (def.maximum !== undefined && num > def.maximum) {\n errors.push(`Maximum value is ${def.maximum}`);\n }\n }\n }\n\n return errors;\n }\n\n _getInputType(def) {\n if (def.format === 'email') return 'email';\n if (def.format === 'password') return 'password';\n if (def.format === 'date') return 'date';\n if (def.format === 'time') return 'time';\n if (def.format === 'datetime-local') return 'datetime-local';\n if (def.format === 'url') return 'url';\n if (def.format === 'tel') return 'tel';\n if (def.type === 'number' || def.type === 'integer') return 'number';\n return 'text';\n }\n\n _renderField(field, def, isRequired) {\n const rawValue = this._values[field] ?? '';\n const errors = this._errors[field] || [];\n const hasError = errors.length > 0;\n\n // XSS Protection: escape all schema-derived content\n const safeField = escapeHTML(field);\n const safeLabel = escapeHTML(def.title || field);\n const safePlaceholder = escapeHTML(def.placeholder || '');\n const safeDescription = escapeHTML(def.description || '');\n const safeValue = escapeHTML(String(rawValue));\n const safeError = hasError ? escapeHTML(errors[0]) : '';\n const disabled = this.disabled || def.readOnly;\n const readonly = this.readonly;\n\n // Boolean → Switch\n if (def.type === 'boolean') {\n return `\n <div class=\"mu-schema-form-field\" data-field=\"${safeField}\">\n <div class=\"mu-schema-form-switch-row\">\n <mu-switch \n ${rawValue ? 'checked' : ''}\n ${disabled ? 'disabled' : ''}\n data-field=\"${safeField}\"\n ></mu-switch>\n <label class=\"mu-schema-form-switch-label\">${safeLabel}</label>\n </div>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n </div>\n `;\n }\n\n // Enum → Dropdown\n if (def.enum) {\n const options = def.enum.map((val, i) => {\n const safeOptValue = escapeHTML(String(val));\n const safeOptLabel = escapeHTML(def.enumLabels?.[i] || String(val));\n return `<mu-option value=\"${safeOptValue}\" ${rawValue === val ? 'selected' : ''}>${safeOptLabel}</mu-option>`;\n }).join('');\n\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-dropdown \n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >\n ${options}\n </mu-dropdown>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Multiline string → Textarea\n if (def.type === 'string' && def.multiline) {\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-textarea\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >${safeValue}</mu-textarea>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Default → Input\n const inputType = this._getInputType(def);\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-input\n type=\"${inputType}\"\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n ${def.minimum !== undefined ? `min=\"${def.minimum}\"` : ''}\n ${def.maximum !== undefined ? `max=\"${def.maximum}\"` : ''}\n ${def.minLength ? `minlength=\"${def.minLength}\"` : ''}\n ${def.maxLength ? `maxlength=\"${def.maxLength}\"` : ''}\n data-field=\"${safeField}\"\n ></mu-input>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n render() {\n if (!this._schema || !this._schema.properties) {\n this.innerHTML = `\n <div class=\"mu-schema-form-empty-state\">\n Set the <code>schema</code> property to generate a form\n </div>\n `;\n return;\n }\n\n const required = this._schema.required || [];\n const fields = Object.entries(this._schema.properties)\n .map(([field, def]) => this._renderField(field, def, required.includes(field)))\n .join('');\n\n this.innerHTML = `\n <div class=\"mu-schema-form ${this.inline ? 'mu-schema-form-inline' : ''}\">\n ${fields}\n \n <div class=\"mu-schema-form-actions\">\n <mu-button variant=\"filled\" data-action=\"submit\" ${this.disabled ? 'disabled' : ''}>\n ${this.submitLabel}\n </mu-button>\n <mu-button variant=\"text\" data-action=\"reset\" ${this.disabled ? 'disabled' : ''}>\n ${this.resetLabel}\n </mu-button>\n </div>\n </div>\n `;\n\n this._attachEventListeners();\n }\n\n _attachEventListeners() {\n // Input changes\n this.querySelectorAll('mu-input').forEach(input => {\n this.listen(input, 'mu-input', (e) => {\n const field = input.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n // Clear error on edit\n if (this._errors[field]) {\n delete this._errors[field];\n input.closest('.mu-schema-form-field').classList.remove('mu-schema-form-field-error');\n input.closest('.mu-schema-form-field').querySelector('.mu-schema-form-error')?.remove();\n }\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Textarea changes\n this.querySelectorAll('mu-textarea').forEach(textarea => {\n this.listen(textarea, 'mu-input', (e) => {\n const field = textarea.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Dropdown changes\n this.querySelectorAll('mu-dropdown').forEach(dropdown => {\n this.listen(dropdown, 'mu-change', (e) => {\n const field = dropdown.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Switch changes\n this.querySelectorAll('mu-switch').forEach(sw => {\n this.listen(sw, 'mu-change', (e) => {\n const field = sw.dataset.field;\n this._values[field] = e.detail.checked;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.checked, values: this.getValues() }, { bubbles: true });\n });\n });\n }\n\n /**\n * Handle actions from MuElement's centralized event delegation\n */\n handleAction(action, target, event) {\n if (action === 'submit') {\n this.submit();\n } else if (action === 'reset') {\n this.reset();\n }\n }\n}\n\nif (!customElements.get('mu-schema-form')) {\n customElements.define('mu-schema-form', MuSchemaForm);\n}\n",
|
|
67
|
+
"/**\n * mu-schema-form.js - Auto-Generated Forms from JSON Schema\n * \n * AI-First form component that generates microUI form fields automatically\n * from a JSON Schema definition. Dramatically reduces boilerplate for form creation.\n * \n * @example\n * <mu-schema-form id=\"user-form\"></mu-schema-form>\n * \n * <script>\n * const form = document.getElementById('user-form');\n * form.schema = {\n * type: 'object',\n * properties: {\n * name: { type: 'string', title: 'Full Name', minLength: 2 },\n * email: { type: 'string', format: 'email', title: 'Email' },\n * role: { type: 'string', enum: ['user', 'admin'], title: 'Role' },\n * active: { type: 'boolean', title: 'Active', default: true }\n * },\n * required: ['name', 'email']\n * };\n * \n * form.addEventListener('mu-submit', (e) => {\n * console.log('Form data:', e.detail); // debug\n * });\n * </script>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\n\nexport class MuSchemaForm extends MuElement {\n static get observedAttributes() {\n return ['submit-label', 'reset-label', 'inline', 'readonly', 'disabled'];\n }\n\n static baseClass = 'mu-schema-form';\n static cssFile = 'schema-form';\n\n constructor() {\n super();\n\n this._schema = null;\n this._values = {};\n this._errors = {};\n this._touched = {};\n }\n\n // === PROPERTIES ===\n\n get schema() {\n return this._schema;\n }\n\n set schema(value) {\n this._schema = value;\n this._initializeValues();\n if (this.isConnected) this.render();\n }\n\n get submitLabel() {\n return this.getAttribute('submit-label') || 'Submit';\n }\n\n get resetLabel() {\n return this.getAttribute('reset-label') || 'Reset';\n }\n\n get inline() {\n return this.hasAttribute('inline');\n }\n\n get readonly() {\n return this.hasAttribute('readonly');\n }\n\n get disabled() {\n return this.hasAttribute('disabled');\n }\n\n // === LIFECYCLE ===\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n // === PUBLIC API ===\n\n /**\n * Get current form values\n * @returns {Object}\n */\n getValues() {\n return { ...this._values };\n }\n\n /**\n * Set form values\n * @param {Object} values\n */\n setValues(values) {\n this._values = { ...this._values, ...values };\n this.render();\n }\n\n /**\n * Validate all fields\n * @returns {boolean} True if valid\n */\n validate() {\n if (!this._schema || !this._schema.properties) return true;\n\n this._errors = {};\n const required = this._schema.required || [];\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n const value = this._values[field];\n const errors = this._validateField(field, value, def, required.includes(field));\n if (errors.length > 0) {\n this._errors[field] = errors;\n }\n }\n\n this.render();\n return Object.keys(this._errors).length === 0;\n }\n\n /**\n * Get validation errors\n * @returns {Object}\n */\n getErrors() {\n return { ...this._errors };\n }\n\n /**\n * Reset form to default values\n */\n reset() {\n this._initializeValues();\n this._errors = {};\n this._touched = {};\n this.render();\n\n this.emit('mu-reset', { values: this.getValues() }, { bubbles: true });\n }\n\n /**\n * Submit the form programmatically\n */\n submit() {\n if (this.validate()) {\n this.emit('mu-submit', this.getValues(), { bubbles: true });\n }\n }\n\n // === INTERNAL METHODS ===\n\n _initializeValues() {\n this._values = {};\n if (!this._schema || !this._schema.properties) return;\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n if (def.default !== undefined) {\n this._values[field] = def.default;\n } else if (def.type === 'boolean') {\n this._values[field] = false;\n } else if (def.type === 'number' || def.type === 'integer') {\n this._values[field] = null;\n } else {\n this._values[field] = '';\n }\n }\n }\n\n _validateField(field, value, def, isRequired) {\n const errors = [];\n\n // Required check\n if (isRequired && (value === '' || value === null || value === undefined)) {\n errors.push(`${def.title || field} is required`);\n return errors;\n }\n\n if (value === '' || value === null || value === undefined) return errors;\n\n // Type-specific validation\n if (def.type === 'string') {\n if (def.minLength && value.length < def.minLength) {\n errors.push(`Minimum ${def.minLength} characters`);\n }\n if (def.maxLength && value.length > def.maxLength) {\n errors.push(`Maximum ${def.maxLength} characters`);\n }\n if (def.pattern && !new RegExp(def.pattern).test(value)) {\n errors.push(def.patternError || 'Invalid format');\n }\n if (def.format === 'email' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push('Invalid email address');\n }\n if (def.format === 'url' && !/^https?:\\/\\/.+/.test(value)) {\n errors.push('Invalid URL');\n }\n }\n\n if (def.type === 'number' || def.type === 'integer') {\n const num = Number(value);\n if (isNaN(num)) {\n errors.push('Must be a number');\n } else {\n if (def.minimum !== undefined && num < def.minimum) {\n errors.push(`Minimum value is ${def.minimum}`);\n }\n if (def.maximum !== undefined && num > def.maximum) {\n errors.push(`Maximum value is ${def.maximum}`);\n }\n }\n }\n\n return errors;\n }\n\n _getInputType(def) {\n if (def.format === 'email') return 'email';\n if (def.format === 'password') return 'password';\n if (def.format === 'date') return 'date';\n if (def.format === 'time') return 'time';\n if (def.format === 'datetime-local') return 'datetime-local';\n if (def.format === 'url') return 'url';\n if (def.format === 'tel') return 'tel';\n if (def.type === 'number' || def.type === 'integer') return 'number';\n return 'text';\n }\n\n _renderField(field, def, isRequired) {\n const rawValue = this._values[field] ?? '';\n const errors = this._errors[field] || [];\n const hasError = errors.length > 0;\n\n // XSS Protection: escape all schema-derived content\n const safeField = escapeHTML(field);\n const safeLabel = escapeHTML(def.title || field);\n const safePlaceholder = escapeHTML(def.placeholder || '');\n const safeDescription = escapeHTML(def.description || '');\n const safeValue = escapeHTML(String(rawValue));\n const safeError = hasError ? escapeHTML(errors[0]) : '';\n const disabled = this.disabled || def.readOnly;\n const readonly = this.readonly;\n\n // Boolean → Switch\n if (def.type === 'boolean') {\n return `\n <div class=\"mu-schema-form-field\" data-field=\"${safeField}\">\n <div class=\"mu-schema-form-switch-row\">\n <mu-switch \n ${rawValue ? 'checked' : ''}\n ${disabled ? 'disabled' : ''}\n data-field=\"${safeField}\"\n ></mu-switch>\n <label class=\"mu-schema-form-switch-label\">${safeLabel}</label>\n </div>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n </div>\n `;\n }\n\n // Enum → Dropdown\n if (def.enum) {\n const options = def.enum.map((val, i) => {\n const safeOptValue = escapeHTML(String(val));\n const safeOptLabel = escapeHTML(def.enumLabels?.[i] || String(val));\n return `<mu-option value=\"${safeOptValue}\" ${rawValue === val ? 'selected' : ''}>${safeOptLabel}</mu-option>`;\n }).join('');\n\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-dropdown \n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >\n ${options}\n </mu-dropdown>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Multiline string → Textarea\n if (def.type === 'string' && def.multiline) {\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-textarea\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >${safeValue}</mu-textarea>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Default → Input\n const inputType = this._getInputType(def);\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-input\n type=\"${inputType}\"\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n ${def.minimum !== undefined ? `min=\"${def.minimum}\"` : ''}\n ${def.maximum !== undefined ? `max=\"${def.maximum}\"` : ''}\n ${def.minLength ? `minlength=\"${def.minLength}\"` : ''}\n ${def.maxLength ? `maxlength=\"${def.maxLength}\"` : ''}\n data-field=\"${safeField}\"\n ></mu-input>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n render() {\n if (!this._schema || !this._schema.properties) {\n this.innerHTML = `\n <div class=\"mu-schema-form-empty-state\">\n Set the <code>schema</code> property to generate a form\n </div>\n `;\n return;\n }\n\n const required = this._schema.required || [];\n const fields = Object.entries(this._schema.properties)\n .map(([field, def]) => this._renderField(field, def, required.includes(field)))\n .join('');\n\n this.innerHTML = `\n <div class=\"mu-schema-form ${this.inline ? 'mu-schema-form-inline' : ''}\">\n ${fields}\n \n <div class=\"mu-schema-form-actions\">\n <mu-button variant=\"filled\" data-action=\"submit\" ${this.disabled ? 'disabled' : ''}>\n ${this.submitLabel}\n </mu-button>\n <mu-button variant=\"text\" data-action=\"reset\" ${this.disabled ? 'disabled' : ''}>\n ${this.resetLabel}\n </mu-button>\n </div>\n </div>\n `;\n\n this._attachEventListeners();\n }\n\n _attachEventListeners() {\n // Input changes\n this.querySelectorAll('mu-input').forEach(input => {\n this.listen(input, 'mu-input', (e) => {\n const field = input.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n // Clear error on edit\n if (this._errors[field]) {\n delete this._errors[field];\n input.closest('.mu-schema-form-field').classList.remove('mu-schema-form-field-error');\n input.closest('.mu-schema-form-field').querySelector('.mu-schema-form-error')?.remove();\n }\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Textarea changes\n this.querySelectorAll('mu-textarea').forEach(textarea => {\n this.listen(textarea, 'mu-input', (e) => {\n const field = textarea.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Dropdown changes\n this.querySelectorAll('mu-dropdown').forEach(dropdown => {\n this.listen(dropdown, 'mu-change', (e) => {\n const field = dropdown.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Switch changes\n this.querySelectorAll('mu-switch').forEach(sw => {\n this.listen(sw, 'mu-change', (e) => {\n const field = sw.dataset.field;\n this._values[field] = e.detail.checked;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.checked, values: this.getValues() }, { bubbles: true });\n });\n });\n }\n\n /**\n * Handle actions from MuElement's centralized event delegation\n */\n handleAction(action, target, event) {\n if (action === 'submit') {\n this.submit();\n } else if (action === 'reset') {\n this.reset();\n }\n }\n}\n\nif (!customElements.get('mu-schema-form')) {\n customElements.define('mu-schema-form', MuSchemaForm);\n}\n",
|
|
68
68
|
"/**\n * mu-prompt-ui.js - AI Agent Prompt UI Components\n * \n * Specialized components for AI agent interfaces:\n * - mu-prompt-input: Enhanced text input with submit button and loading states\n * - mu-code-block: Syntax-highlighted code display with copy button\n * - mu-agent-toolbar: Action buttons for AI responses (copy, regenerate, etc.)\n * - mu-message-bubble: Chat message bubbles with role-based styling\n * \n * @example\n * <mu-prompt-input placeholder=\"Ask anything...\" loading></mu-prompt-input>\n * \n * <mu-message-bubble role=\"user\">\n * How do I sort an array?\n * </mu-message-bubble>\n * \n * <mu-message-bubble role=\"assistant\">\n * Here's how to sort an array:\n * <mu-code-block language=\"javascript\">\n * const sorted = array.sort((a, b) => a - b);\n * </mu-code-block>\n * <mu-agent-toolbar></mu-agent-toolbar>\n * </mu-message-bubble>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\n\n// ============================================\n// MU-PROMPT-INPUT\n// ============================================\nexport class MuPromptInput extends MuElement {\n static get observedAttributes() {\n return ['placeholder', 'loading', 'disabled', 'multiline'];\n }\n\n static baseClass = 'mu-prompt-input';\n static cssFile = 'prompt-ui';\n\n constructor() {\n super();\n this._value = '';\n }\n\n get placeholder() {\n return this.getAttribute('placeholder') || 'Type a message...';\n }\n\n get loading() {\n return this.hasAttribute('loading');\n }\n\n set loading(val) {\n if (val) this.setAttribute('loading', '');\n else this.removeAttribute('loading');\n }\n\n get disabled() {\n return this.hasAttribute('disabled');\n }\n\n get multiline() {\n return this.hasAttribute('multiline');\n }\n\n get value() {\n return this._value;\n }\n\n set value(val) {\n this._value = val;\n const input = this.querySelector('.mu-prompt-input-field');\n if (input) input.value = val;\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n _submit() {\n if (this.loading || this.disabled || !this._value.trim()) return;\n\n this.dispatchEvent(new CustomEvent('mu-submit', {\n bubbles: true,\n detail: { value: this._value.trim() }\n }));\n }\n\n _handleKeydown(e) {\n if (e.key === 'Enter' && !e.shiftKey && !this.multiline) {\n e.preventDefault();\n this._submit();\n } else if (e.key === 'Enter' && e.metaKey) {\n e.preventDefault();\n this._submit();\n }\n }\n\n render() {\n const isTextarea = this.multiline;\n\n this.innerHTML = `\n <div class=\"mu-prompt-input-container ${this.loading ? 'mu-prompt-input-loading' : ''}\">\n ${isTextarea\n ? `<textarea class=\"mu-prompt-input-field\" placeholder=\"${this.placeholder}\" ${this.disabled ? 'disabled' : ''}></textarea>`\n : `<input class=\"mu-prompt-input-field\" type=\"text\" placeholder=\"${this.placeholder}\" ${this.disabled ? 'disabled' : ''} />`\n }\n <button class=\"mu-prompt-input-submit\" ${this.disabled || this.loading ? 'disabled' : ''}>\n ${this.loading\n ? '<div class=\"mu-prompt-input-spinner\"></div>'\n : '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2.01 21L23 12 2.01 3 2 10l15 2-15 2z\"/></svg>'\n }\n </button>\n </div>\n `;\n\n const input = this.querySelector('.mu-prompt-input-field');\n const submitBtn = this.querySelector('.mu-prompt-input-submit');\n\n input.value = this._value;\n this.listen(input, 'input', (e) => {\n this._value = e.target.value;\n this.dispatchEvent(new CustomEvent('mu-input', {\n bubbles: true,\n detail: { value: this._value }\n }));\n });\n this.listen(input, 'keydown', (e) => this._handleKeydown(e));\n this.listen(submitBtn, 'click', () => this._submit());\n }\n}\n\n// ============================================\n// MU-CODE-BLOCK\n// ============================================\nexport class MuCodeBlock extends MuElement {\n static get observedAttributes() {\n return ['language', 'filename'];\n }\n\n static baseClass = 'mu-code-block';\n\n constructor() {\n super();\n }\n\n get language() {\n return this.getAttribute('language') || 'text';\n }\n\n get filename() {\n return this.getAttribute('filename');\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n async _copy() {\n const code = this.textContent.trim();\n try {\n await navigator.clipboard.writeText(code);\n const btn = this.querySelector('.mu-code-block-copy-btn');\n btn.textContent = '✓ Copied';\n this.setTimeout(() => {\n btn.textContent = 'Copy';\n }, 2000);\n } catch (e) {\n console.error('Failed to copy:', e);\n }\n }\n\n render() {\n const code = this.textContent.trim();\n\n this.innerHTML = `\n <div class=\"mu-code-block-container\">\n <div class=\"mu-code-block-header\">\n ${this.filename\n ? `<span class=\"mu-code-block-filename\">${this.filename}</span>`\n : `<span class=\"mu-code-block-language\">${this.language}</span>`\n }\n <button class=\"mu-code-block-copy-btn\">Copy</button>\n </div>\n <pre class=\"mu-code-block-pre\"><code>${escapeHTML(code)}</code></pre>\n </div>\n `;\n\n this.listen(this.querySelector('.mu-code-block-copy-btn'), 'click', () => this._copy());\n }\n\n // Using centralized escapeHTML from core/utils.js for XSS safety\n}\n\n// ============================================\n// MU-AGENT-TOOLBAR\n// ============================================\nexport class MuAgentToolbar extends MuElement {\n static get observedAttributes() {\n return ['actions'];\n }\n\n static baseClass = 'mu-agent-toolbar';\n\n constructor() {\n super();\n }\n\n get actions() {\n const attr = this.getAttribute('actions');\n return attr ? attr.split(',').map(a => a.trim()) : ['copy', 'regenerate', 'thumbsUp', 'thumbsDown'];\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n\n\n render() {\n const actionButtons = {\n copy: { icon: '📋', label: 'Copy' },\n regenerate: { icon: '🔄', label: 'Regenerate' },\n thumbsUp: { icon: '👍', label: 'Good response' },\n thumbsDown: { icon: '👎', label: 'Poor response' },\n edit: { icon: '✏️', label: 'Edit' },\n share: { icon: '↗️', label: 'Share' }\n };\n\n const buttons = this.actions\n .filter(a => actionButtons[a])\n .map(a => `\n <button class=\"mu-agent-toolbar-btn\" data-action=\"${a}\" title=\"${actionButtons[a].label}\">\n ${actionButtons[a].icon}\n </button>\n `).join('');\n\n this.innerHTML = buttons;\n }\n\n /**\n * Handle actions from MuElement's centralized event delegation\n */\n handleAction(action, target, event) {\n this.dispatchEvent(new CustomEvent('mu-action', {\n bubbles: true,\n detail: { action }\n }));\n }\n}\n\n// ============================================\n// MU-MESSAGE-BUBBLE\n// ============================================\nexport class MuMessageBubble extends MuElement {\n static get observedAttributes() {\n return ['role', 'avatar', 'name', 'timestamp'];\n }\n\n static baseClass = 'mu-message-bubble';\n\n constructor() {\n super();\n this._originalContent = null;\n }\n\n get role() {\n return this.getAttribute('role') || 'user';\n }\n\n get avatar() {\n return this.getAttribute('avatar');\n }\n\n get name() {\n return this.getAttribute('name') || (this.role === 'user' ? 'You' : 'Assistant');\n }\n\n get timestamp() {\n return this.getAttribute('timestamp');\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Capture original children before render\n if (!this._originalContent) {\n this._originalContent = Array.from(this.childNodes);\n }\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n render() {\n const isUser = this.role === 'user';\n const defaultAvatar = isUser ? '👤' : '🤖';\n const roleClass = isUser ? 'mu-message-bubble-user' : 'mu-message-bubble-assistant';\n\n // Create structure\n const message = document.createElement('div');\n message.className = `mu-message-bubble-message ${roleClass}`;\n\n // Avatar\n const avatar = document.createElement('div');\n avatar.className = `mu-message-bubble-avatar ${roleClass}`;\n if (this.avatar) {\n const img = document.createElement('img');\n img.src = this.avatar;\n img.alt = this.name;\n avatar.appendChild(img);\n } else {\n avatar.textContent = defaultAvatar;\n }\n message.appendChild(avatar);\n\n // Content wrapper\n const content = document.createElement('div');\n content.className = 'mu-message-bubble-content';\n\n // Header\n const header = document.createElement('div');\n header.className = `mu-message-bubble-header ${roleClass}`;\n const nameSpan = document.createElement('span');\n nameSpan.className = 'mu-message-bubble-name';\n nameSpan.textContent = this.name;\n header.appendChild(nameSpan);\n if (this.timestamp) {\n const timestampSpan = document.createElement('span');\n timestampSpan.className = 'mu-message-bubble-timestamp';\n timestampSpan.textContent = this.timestamp;\n header.appendChild(timestampSpan);\n }\n content.appendChild(header);\n\n // Bubble with original content\n const bubble = document.createElement('div');\n bubble.className = `mu-message-bubble-bubble ${roleClass}`;\n if (this._originalContent) {\n this._originalContent.forEach(child => bubble.appendChild(child.cloneNode(true)));\n }\n content.appendChild(bubble);\n\n message.appendChild(content);\n\n // Set HTML\n this.innerHTML = '';\n this.appendChild(message);\n }\n}\n\n// Register all components\nif (!customElements.get('mu-prompt-input')) {\n customElements.define('mu-prompt-input', MuPromptInput);\n}\nif (!customElements.get('mu-code-block')) {\n customElements.define('mu-code-block', MuCodeBlock);\n}\nif (!customElements.get('mu-agent-toolbar')) {\n customElements.define('mu-agent-toolbar', MuAgentToolbar);\n}\nif (!customElements.get('mu-message-bubble')) {\n customElements.define('mu-message-bubble', MuMessageBubble);\n}\n",
|
|
69
69
|
"/**\n * @fileoverview mu-error-boundary - Error Boundary Component\n * \n * Catches rendering errors in child components and displays fallback UI.\n * Critical for large-scale applications where one component error\n * should not crash the entire application.\n * \n * Based on 2024 enterprise error recovery patterns.\n * \n * Usage:\n * <mu-error-boundary fallback=\"<p>Something went wrong</p>\">\n * <mu-complex-widget></mu-complex-widget>\n * </mu-error-boundary>\n * \n * With error handler:\n * <mu-error-boundary onerror=\"customHandler(event.detail)\">\n * <mu-data-table :items=\"${data}\"></mu-data-table>\n * </mu-error-boundary>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\n\n/**\n * Global error registry for agent observability\n * @type {Array<{timestamp: number, component: string, error: Error, context: Object}>}\n */\nconst errorRegistry = [];\n\n/**\n * Get all caught errors for agent debugging\n * @returns {Array<Object>}\n */\nexport function getErrors() {\n return [...errorRegistry];\n}\n\n/**\n * Clear error registry\n */\nexport function clearErrors() {\n errorRegistry.length = 0;\n}\n\nexport class MuErrorBoundary extends MuElement {\n static baseClass = 'mu-error-boundary';\n static observedAttributes = ['fallback'];\n\n #hasError = false;\n #error = null;\n #originalContent = '';\n\n connectedCallback() {\n super.connectedCallback();\n\n // Store original content for potential recovery\n this.#originalContent = this.innerHTML;\n\n // Listen for errors from child components\n this.listen(this, 'error', this.#handleError.bind(this));\n\n // Also catch unhandled errors from child scripts\n this.#setupGlobalErrorHandler();\n }\n\n #setupGlobalErrorHandler() {\n // Capture errors that occur within this boundary\n const originalHandler = window.onerror;\n\n window.onerror = (message, source, lineno, colno, error) => {\n // Check if error originated from within this boundary\n if (this.#isErrorFromChild(error)) {\n this.#handleError({ detail: { error, message } });\n return true; // Prevent propagation\n }\n\n // Call original handler if exists\n if (originalHandler) {\n return originalHandler(message, source, lineno, colno, error);\n }\n return false;\n };\n }\n\n #isErrorFromChild(error) {\n // Best-effort check if error is from child component\n if (!error?.stack) return false;\n\n const childTags = Array.from(this.querySelectorAll('*'))\n .filter(el => el.tagName.startsWith('MU-'))\n .map(el => el.tagName.toLowerCase());\n\n return childTags.some(tag => error.stack.includes(tag));\n }\n\n #handleError(event) {\n const error = event?.detail?.error || event?.error || event;\n\n if (this.#hasError) return; // Already showing fallback\n\n this.#hasError = true;\n this.#error = error;\n\n // Register error for agent observability\n errorRegistry.push({\n timestamp: Date.now(),\n component: this.id || 'mu-error-boundary',\n error: {\n message: error?.message || String(error),\n stack: error?.stack,\n name: error?.name\n },\n context: {\n originalContent: this.#originalContent.slice(0, 200),\n parentUrl: window.location.href\n }\n });\n\n // Keep only last 50 errors\n if (errorRegistry.length > 50) {\n errorRegistry.shift();\n }\n\n // Show fallback UI\n this.#renderFallback();\n\n // Emit event for custom handling\n this.emit('mu-error', {\n error,\n boundary: this,\n recover: () => this.recover()\n });\n\n // Prevent event propagation\n event?.stopPropagation?.();\n }\n\n #renderFallback() {\n const fallback = this.getAttribute('fallback');\n\n if (fallback) {\n this.innerHTML = fallback;\n } else {\n // Default fallback UI\n this.innerHTML = `\n <div class=\"mu-error-boundary__fallback\" role=\"alert\" aria-live=\"polite\">\n <mu-icon name=\"error\" style=\"--mu-icon-size: 48px; color: var(--md-sys-color-error);\"></mu-icon>\n <p class=\"mu-error-boundary__title\">Something went wrong</p>\n <p class=\"mu-error-boundary__message\">${this.#error?.message || 'An unexpected error occurred'}</p>\n <mu-button variant=\"outlined\" onclick=\"this.closest('mu-error-boundary').recover()\">\n Try Again\n </mu-button>\n </div>\n `;\n }\n\n this.classList.add('has-error');\n }\n\n render() {\n // Apply base styles\n this.style.display = 'block';\n }\n\n /**\n * Attempt to recover by restoring original content\n */\n recover() {\n this.#hasError = false;\n this.#error = null;\n this.classList.remove('has-error');\n this.innerHTML = this.#originalContent;\n\n this.emit('mu-recover');\n }\n\n /**\n * Check if boundary is currently showing error\n */\n get hasError() {\n return this.#hasError;\n }\n\n /**\n * Get the current error\n */\n get error() {\n return this.#error;\n }\n}\n\ndefine('mu-error-boundary', MuErrorBoundary);\n",
|
|
70
70
|
"/**\n * @fileoverview mu-code - Code Block Component with Syntax Highlighting\n * \n * Usage: <mu-code language=\"html\">...</mu-code>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\n\nexport class MuCode extends MuElement {\n static baseClass = 'mu-code';\n static observedAttributes = ['language'];\n\n #originalCode = null;\n\n connectedCallback() {\n super.connectedCallback();\n // Store original code before render\n if (!this.#originalCode && !this.querySelector('.mu-code__content')) {\n this.#originalCode = this.textContent;\n }\n }\n\n render() {\n // Idempotent: skip if already rendered\n if (this.querySelector('.mu-code__content')) {\n return;\n }\n\n const language = this.attr('language', 'html');\n\n // Get raw code - use innerHTML to preserve any pre-escaped entities\n // textContent would decode < to < which we don't want\n const rawCode = this.#originalCode || this.innerHTML;\n\n // First decode any HTML entities that might already be in the content\n // This handles cases where the HTML source uses < > etc.\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = rawCode;\n const decodedCode = tempDiv.textContent;\n\n // Dedent: remove common leading whitespace from all lines\n const dedentedCode = this.#dedent(decodedCode);\n\n // Auto-indent: apply proper code formatting based on language\n const formattedCode = this.#autoIndent(dedentedCode, language);\n\n // Store formatted version for copy button\n const copyableCode = formattedCode;\n\n // Now escape for display - only &, <, > need escaping for code\n // Do NOT escape quotes as they are safe inside <code> elements\n const escaped = formattedCode\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n\n // Basic syntax highlighting\n const highlighted = this.#highlight(escaped, language);\n\n this.innerHTML = `\n <div class=\"mu-code__header\">\n <span class=\"mu-code__language\">${language.toUpperCase()}</span>\n <button class=\"mu-code__copy\" title=\"Copy code\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/>\n <path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/>\n </svg>\n </button>\n </div>\n <pre class=\"mu-code__pre\"><code class=\"mu-code__content\">${highlighted}</code></pre>\n `;\n\n this.#applyStyles();\n this.#setupCopyButton(copyableCode);\n }\n\n #highlight(code, language) {\n // Shared marker helper to avoid regex self-matching\n // IMPORTANT: Uses Unicode Private Use Area characters + letters to avoid\n // collision with number regex which matches \\b\\d+\\b\n const createHighlighter = () => {\n const markers = [];\n let markerIndex = 0;\n\n const addMarker = (content, className) => {\n // Create a truly unique marker that no highlighting regex can match:\n // Encode the index as a single Unicode PUA character (offset by 0xE100)\n // This way there are NO digits or word characters in the marker at all\n const idx = markerIndex++;\n const markerChar = String.fromCharCode(0xE100 + idx);\n const start = String.fromCharCode(0xE000);\n const end = String.fromCharCode(0xE001);\n const marker = start + markerChar + end;\n markers.push({ marker, content, className });\n return marker;\n };\n\n const resolveMarkers = (result) => {\n for (const { marker, content, className } of markers) {\n result = result.replace(marker, `<span class=\"${className}\">${content}</span>`);\n }\n return result;\n };\n\n return { addMarker, resolveMarkers };\n };\n\n // HTML highlighting\n if (language === 'html' || language === 'xml') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // HTML comments: <!-- ... -->\n result = result.replace(/(<!--[\\s\\S]*?-->)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // DOCTYPE\n result = result.replace(/(<!DOCTYPE[^&]*>)/gi, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Strings (attribute values)\n result = result.replace(/(\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Attributes (name= pattern)\n result = result.replace(/(\\s)([\\w-]+)(=)/g, (match, space, attr, eq) =>\n `${space}${addMarker(attr, 'mu-code__attr')}${eq}`);\n\n // Tags\n result = result.replace(/(<\\/?)([\\w-]+)/g, (match, bracket, tag) =>\n `${bracket}${addMarker(tag, 'mu-code__tag')}`);\n\n return resolveMarkers(result);\n }\n\n // JavaScript highlighting\n if (language === 'javascript' || language === 'js' || language === 'typescript' || language === 'ts') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Multi-line comments /* ... */\n result = result.replace(/(\\/\\*[\\s\\S]*?\\*\\/)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Single-line comments\n result = result.replace(/(\\/\\/.*)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Template literals (backticks)\n result = result.replace(/(`[^`]*`)/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Strings\n result = result.replace(/('[^']*'|\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Numbers (including decimals and scientific notation)\n result = result.replace(/\\b(\\d+\\.?\\d*(?:e[+-]?\\d+)?)\\b/gi, (match) =>\n addMarker(match, 'mu-code__number'));\n\n // Keywords (expanded list)\n result = result.replace(/\\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|class|extends|import|export|from|default|async|await|new|this|super|typeof|instanceof|in|of|try|catch|finally|throw|yield|static|get|set)\\b/g, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Boolean/null/undefined\n result = result.replace(/\\b(true|false|null|undefined|NaN|Infinity)\\b/g, (match) =>\n addMarker(match, 'mu-code__builtin'));\n\n // Function names (word followed by parenthesis)\n result = result.replace(/\\b([a-zA-Z_]\\w*)\\s*(?=\\()/g, (match, name) =>\n addMarker(name, 'mu-code__function'));\n\n return resolveMarkers(result);\n }\n\n // CSS highlighting\n if (language === 'css' || language === 'scss' || language === 'sass') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Comments\n result = result.replace(/(\\/\\*[\\s\\S]*?\\*\\/)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Strings\n result = result.replace(/('[^']*'|\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // At-rules (@media, @keyframes, etc.)\n result = result.replace(/(@[\\w-]+)/g, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Property values with units\n result = result.replace(/\\b(\\d+\\.?\\d*)(px|em|rem|%|vh|vw|s|ms|deg|fr)\\b/g, (match, num, unit) =>\n `${addMarker(num, 'mu-code__number')}${addMarker(unit, 'mu-code__builtin')}`);\n\n // Numbers without units\n result = result.replace(/\\b(\\d+\\.?\\d*)\\b/g, (match) =>\n addMarker(match, 'mu-code__number'));\n\n // Hex colors\n result = result.replace(/(#[0-9a-fA-F]{3,8})\\b/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // CSS properties (word followed by colon)\n result = result.replace(/\\b([\\w-]+)\\s*:/g, (match, prop) =>\n `${addMarker(prop, 'mu-code__attr')}:`);\n\n // Selectors (. # or element at start of line or after comma/brace)\n result = result.replace(/(^|[{,\\s])([.#]?[\\w-]+)(?=\\s*[{,])/gm, (match, before, selector) =>\n `${before}${addMarker(selector, 'mu-code__tag')}`);\n\n return resolveMarkers(result);\n }\n\n // JSON highlighting\n if (language === 'json') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Strings (keys will be handled separately)\n result = result.replace(/(\"[^\"]*\")\\s*:/g, (match, key) =>\n `${addMarker(key, 'mu-code__attr')}:`);\n\n // String values\n result = result.replace(/:\\s*(\"[^\"]*\")/g, (match, val) =>\n `: ${addMarker(val, 'mu-code__string')}`);\n\n // Numbers\n result = result.replace(/:\\s*(-?\\d+\\.?\\d*)/g, (match, num) =>\n `: ${addMarker(num, 'mu-code__number')}`);\n\n // Booleans and null\n result = result.replace(/\\b(true|false|null)\\b/g, (match) =>\n addMarker(match, 'mu-code__builtin'));\n\n return resolveMarkers(result);\n }\n\n // Shell/Bash highlighting\n if (language === 'bash' || language === 'sh' || language === 'shell') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Comments\n result = result.replace(/(#.*)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Strings\n result = result.replace(/('[^']*'|\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Variables $VAR and ${VAR}\n result = result.replace(/(\\$\\{?\\w+\\}?)/g, (match) =>\n addMarker(match, 'mu-code__attr'));\n\n // Common commands\n result = result.replace(/\\b(cd|ls|echo|cat|grep|find|sudo|npm|npx|bun|node|git|curl|wget|mkdir|rm|cp|mv|chmod|chown)\\b/g, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Flags\n result = result.replace(/(\\s)(--?[\\w-]+)/g, (match, space, flag) =>\n `${space}${addMarker(flag, 'mu-code__builtin')}`);\n\n return resolveMarkers(result);\n }\n\n // No highlighting for unknown languages\n return code;\n }\n\n /**\n * SOTA Dedent - Remove common leading whitespace from all lines\n * Based on TC39 String.dedent proposal and npm 'dedent' package algorithm\n * \n * Algorithm:\n * 1. Split into lines, trim leading/trailing empty lines\n * 2. Find minimum indent across ALL non-empty lines\n * 3. Remove exactly that amount from start of each line\n * \n * This preserves relative indentation within the code.\n */\n #dedent(code) {\n // Split into lines\n let lines = code.split('\\n');\n\n // Remove leading empty lines\n while (lines.length > 0 && lines[0].trim() === '') {\n lines.shift();\n }\n // Remove trailing empty lines\n while (lines.length > 0 && lines[lines.length - 1].trim() === '') {\n lines.pop();\n }\n\n if (lines.length === 0) return '';\n\n // Find minimum indent across ALL non-empty lines\n let minIndent = Infinity;\n for (const line of lines) {\n // Skip empty lines when calculating min indent\n if (line.trim() === '') continue;\n\n const match = line.match(/^(\\s*)/);\n const indent = match ? match[1].length : 0;\n minIndent = Math.min(minIndent, indent);\n }\n\n // If no common indent found, return as-is\n if (minIndent === Infinity || minIndent === 0) {\n return lines.join('\\n');\n }\n\n // Remove exactly minIndent spaces from start of each line\n return lines.map(line => {\n if (line.trim() === '') return ''; // Keep empty lines as empty\n return line.slice(minIndent);\n }).join('\\n');\n }\n\n /**\n * SOTA Auto-Indent - Lightweight bracket-counting code formatter\n * Inspired by js-beautify but minimal (~30 lines vs ~50KB)\n * \n * Supports: JavaScript/JSON ({} blocks), HTML (<> tags), CSS ({} rules)\n * \n * Algorithm:\n * 1. Strip existing indentation (each line trimmed)\n * 2. Track indent level based on opening/closing brackets\n * 3. Apply consistent 4-space indentation\n */\n #autoIndent(code, language) {\n const INDENT = ' '; // 4 spaces\n const lines = code.split('\\n');\n const result = [];\n let indentLevel = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n\n // Skip empty lines\n if (line === '') {\n result.push('');\n continue;\n }\n\n // Calculate bracket changes for this line\n let openBrackets = 0;\n let closeBrackets = 0;\n\n if (language === 'html') {\n // HTML: count opening/closing tags\n // Opening: <tag> or <tag attr>\n // Closing: </tag> or self-closing />\n const openTags = (line.match(/<[a-zA-Z][^>]*(?<!\\/)\\s*>/g) || []).length;\n const closeTags = (line.match(/<\\/[^>]+>/g) || []).length;\n const selfClosing = (line.match(/<[^>]+\\/>/g) || []).length;\n openBrackets = openTags - selfClosing;\n closeBrackets = closeTags;\n } else {\n // JS/CSS/JSON: count { and }\n // Avoid counting brackets inside strings (basic heuristic)\n let inString = false;\n let stringChar = '';\n for (let j = 0; j < line.length; j++) {\n const char = line[j];\n const prev = j > 0 ? line[j - 1] : '';\n\n if ((char === '\"' || char === \"'\" || char === '`') && prev !== '\\\\') {\n if (!inString) {\n inString = true;\n stringChar = char;\n } else if (char === stringChar) {\n inString = false;\n }\n }\n\n if (!inString) {\n if (char === '{' || char === '[' || char === '(') openBrackets++;\n if (char === '}' || char === ']' || char === ')') closeBrackets++;\n }\n }\n }\n\n // Determine if line starts with closing bracket (decrease indent BEFORE this line)\n const startsWithClose = language === 'html'\n ? line.startsWith('</')\n : /^[}\\]\\)]/.test(line);\n\n // Apply closing brackets that start the line BEFORE indenting\n if (startsWithClose && closeBrackets > 0) {\n indentLevel = Math.max(0, indentLevel - 1);\n closeBrackets--; // Already accounted for\n }\n\n // Add the indented line\n result.push(INDENT.repeat(indentLevel) + line);\n\n // Update indent level for next line\n // Net change: opens increase, closes decrease\n indentLevel = Math.max(0, indentLevel + openBrackets - closeBrackets);\n }\n\n return result.join('\\n');\n }\n\n #applyStyles() {\n this.style.display = 'block';\n this.style.background = 'var(--md-sys-color-surface-container-highest)';\n\n // Skip border-radius when embedded in mu-example for seamless integration\n const isEmbedded = this.closest('mu-example');\n if (isEmbedded) {\n // Only top corners flat to align with header, bottom stays rounded\n this.style.borderTopLeftRadius = '0';\n this.style.borderTopRightRadius = '0';\n this.style.borderBottomLeftRadius = 'var(--md-sys-shape-corner-medium)';\n this.style.borderBottomRightRadius = 'var(--md-sys-shape-corner-medium)';\n } else {\n this.style.borderRadius = 'var(--md-sys-shape-corner-medium)';\n }\n\n this.style.overflow = 'hidden';\n this.style.fontFamily = \"'Fira Code', 'Consolas', monospace\";\n this.style.fontSize = '13px';\n\n const header = this.querySelector('.mu-code__header');\n if (header) {\n header.style.display = 'flex';\n header.style.justifyContent = 'space-between';\n header.style.alignItems = 'center';\n header.style.padding = '8px 16px';\n header.style.background = 'var(--md-sys-color-surface-container)';\n header.style.borderBottom = '1px solid var(--md-sys-color-outline-variant)';\n }\n\n const lang = this.querySelector('.mu-code__language');\n if (lang) {\n lang.style.fontSize = '11px';\n lang.style.fontWeight = '600';\n lang.style.color = 'var(--md-sys-color-primary)';\n lang.style.letterSpacing = '0.5px';\n }\n\n const copyBtn = this.querySelector('.mu-code__copy');\n if (copyBtn) {\n copyBtn.style.background = 'transparent';\n copyBtn.style.border = 'none';\n copyBtn.style.cursor = 'pointer';\n copyBtn.style.color = 'var(--md-sys-color-on-surface-variant)';\n copyBtn.style.padding = '4px';\n copyBtn.style.borderRadius = '4px';\n copyBtn.style.display = 'flex';\n copyBtn.style.transition = 'all 0.2s ease';\n }\n\n const pre = this.querySelector('.mu-code__pre');\n if (pre) {\n pre.style.margin = '0';\n pre.style.padding = '16px';\n pre.style.overflow = 'auto';\n pre.style.maxHeight = '400px';\n }\n\n const content = this.querySelector('.mu-code__content');\n if (content) {\n content.style.color = 'var(--md-sys-color-on-surface)';\n }\n\n // Syntax highlighting colors\n const style = document.createElement('style');\n style.textContent = `\n .mu-code__tag { color: var(--md-sys-color-primary); }\n .mu-code__attr { color: var(--md-sys-color-tertiary); }\n .mu-code__string { color: var(--md-sys-color-secondary); }\n .mu-code__keyword { color: var(--md-sys-color-primary); font-weight: 500; }\n .mu-code__comment { color: var(--md-sys-color-outline); font-style: italic; }\n .mu-code__number { color: var(--md-sys-color-error); }\n .mu-code__function { color: var(--md-sys-color-tertiary); }\n .mu-code__builtin { color: var(--md-sys-color-primary); font-weight: 500; }\n .mu-code__copy:hover { \n background: var(--md-sys-color-primary-container) !important;\n color: var(--md-sys-color-on-primary-container) !important;\n }\n .mu-code__copy:active {\n background: var(--md-sys-color-primary) !important;\n color: var(--md-sys-color-on-primary) !important;\n transform: scale(0.95);\n }\n .mu-code__copy.copied {\n background: #c8e6c9 !important;\n color: #1b5e20 !important;\n }\n `;\n // Remove old styles and add new ones to ensure latest CSS\n const existingStyle = document.getElementById('mu-code-styles');\n if (existingStyle) {\n existingStyle.remove();\n }\n style.id = 'mu-code-styles';\n document.head.appendChild(style);\n }\n\n #setupCopyButton(originalCode) {\n const btn = this.querySelector('.mu-code__copy');\n if (btn) {\n this.listen(btn, 'click', async () => {\n try {\n await navigator.clipboard.writeText(originalCode.trim());\n // Add copied class for visual feedback\n btn.classList.add('copied');\n btn.innerHTML = `\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n `;\n this.setTimeout(() => {\n btn.classList.remove('copied');\n btn.innerHTML = `\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/>\n <path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/>\n </svg>\n `;\n }, 2000);\n } catch (e) {\n console.error('Copy failed:', e);\n }\n });\n }\n }\n}\n\ndefine('mu-code', MuCode);\n",
|
package/dist/microui.min.js.map
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"/**\n * @fileoverview mu-repeat - Efficient List Rendering with Keyed Diffing\n * \n * Renders arrays efficiently by only updating changed items.\n * Uses key-based reconciliation like React/Vue.\n * \n * Usage:\n * <mu-repeat id=\"list\"></mu-repeat>\n * \n * const list = document.getElementById('list');\n * list.items = users;\n * list.keyFn = (user) => user.id;\n * list.renderItem = (user) => `<div>${user.name}</div>`;\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { scheduler } from '../core/render.js';\n\nexport class MuRepeat extends MuElement {\n static baseClass = 'mu-repeat';\n static observedAttributes = [];\n\n #items = [];\n #keyFn = (item, index) => index;\n #renderItem = (item) => `<div>${JSON.stringify(item)}</div>`;\n #itemNodes = new Map(); // key -> DOM element\n\n set items(arr) {\n const oldItems = this.#items;\n this.#items = arr || [];\n\n // Schedule batched update\n scheduler.schedule(() => this.#reconcile(oldItems));\n }\n\n get items() {\n return this.#items;\n }\n\n set keyFn(fn) {\n this.#keyFn = fn;\n }\n\n set renderItem(fn) {\n this.#renderItem = fn;\n }\n\n render() {\n this.style.display = 'contents';\n }\n\n /**\n * Efficient reconciliation - only updates changed items\n */\n #reconcile(oldItems) {\n const newKeys = new Set();\n const fragment = document.createDocumentFragment();\n\n // Build set of new keys\n for (let i = 0; i < this.#items.length; i++) {\n const key = this.#keyFn(this.#items[i], i);\n newKeys.add(key);\n }\n\n // Remove items that no longer exist\n for (const [key, node] of this.#itemNodes) {\n if (!newKeys.has(key)) {\n node.remove();\n this.#itemNodes.delete(key);\n }\n }\n\n // Add/update items\n let prevNode = null;\n\n for (let i = 0; i < this.#items.length; i++) {\n const item = this.#items[i];\n const key = this.#keyFn(item, i);\n\n let node = this.#itemNodes.get(key);\n\n if (!node) {\n // Create new node\n const wrapper = document.createElement('div');\n wrapper.innerHTML = this.#renderItem(item, i);\n node = wrapper.firstElementChild || wrapper;\n node.dataset.muKey = key;\n this.#itemNodes.set(key, node);\n } else {\n // Update existing node content\n const newContent = this.#renderItem(item, i);\n if (node.outerHTML !== newContent) {\n const wrapper = document.createElement('div');\n wrapper.innerHTML = newContent;\n const newNode = wrapper.firstElementChild || wrapper;\n newNode.dataset.muKey = key;\n node.replaceWith(newNode);\n this.#itemNodes.set(key, newNode);\n node = newNode;\n }\n }\n\n // Position correctly\n if (prevNode) {\n if (node.previousElementSibling !== prevNode) {\n prevNode.after(node);\n }\n } else {\n if (this.firstElementChild !== node) {\n this.prepend(node);\n }\n }\n\n prevNode = node;\n }\n }\n\n /**\n * Get DOM element for a key\n */\n getElement(key) {\n return this.#itemNodes.get(key);\n }\n\n /**\n * Force full re-render\n */\n refresh() {\n this.#itemNodes.clear();\n this.innerHTML = '';\n scheduler.schedule(() => this.#reconcile([]));\n }\n}\n\ndefine('mu-repeat', MuRepeat);\n",
|
|
65
65
|
"/**\n * @fileoverview mu-fetch - Declarative Data Fetcher Component\n * Agent-friendly component for fetching data with automatic loading/error states\n * \n * @example\n * <mu-fetch url=\"/api/users\" id=\"users\">\n * <template slot=\"loading\"><mu-skeleton></mu-skeleton></template>\n * <template slot=\"error\"><mu-alert variant=\"error\">${error}</mu-alert></template>\n * </mu-fetch>\n * \n * // Access data\n * const fetcher = document.getElementById('users');\n * fetcher.data; // The fetched data\n * fetcher.refetch(); // Re-fetch data\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\n\n/**\n * @typedef {'idle'|'loading'|'success'|'error'} FetchState\n */\n\n/**\n * @typedef {Object} FetchResult\n * @property {FetchState} state\n * @property {any} data\n * @property {Error|null} error\n */\n\nexport class MuFetch extends MuElement {\n static baseClass = 'mu-fetch';\n static observedAttributes = ['url', 'method', 'auto', 'interval'];\n\n /** @type {FetchState} */\n state = 'idle';\n\n /** @type {any} */\n data = null;\n\n /** @type {Error|null} */\n error = null;\n\n /** @type {AbortController|null} */\n _controller = null;\n\n /** @type {number|null} */\n _intervalId = null;\n\n connectedCallback() {\n // Store templates before render\n this._loadingTemplate = this.querySelector('template[slot=\"loading\"]')?.innerHTML ||\n '<mu-spinner></mu-spinner>';\n this._errorTemplate = this.querySelector('template[slot=\"error\"]')?.innerHTML ||\n '<mu-alert variant=\"error\">Error loading data</mu-alert>';\n this._emptyTemplate = this.querySelector('template[slot=\"empty\"]')?.innerHTML ||\n '<p>No data</p>';\n\n super.connectedCallback();\n\n // Auto-fetch on connect\n if (this.has('auto') || this.hasAttribute('url')) {\n this.fetch();\n }\n\n // Setup interval refresh\n const interval = parseInt(this.attr('interval', '0'));\n if (interval > 0) {\n this.setInterval(() => this.fetch(), interval);\n }\n }\n\n render() {\n // Render based on current state\n this.innerHTML = ''; // Clear content\n\n switch (this.state) {\n case 'loading':\n // Safe: _loadingTemplate is trusted or empty, but let's wrap it container\n const loadingContainer = document.createElement('div');\n loadingContainer.className = 'mu-fetch__loading';\n loadingContainer.innerHTML = this._loadingTemplate; // _loadingTemplate comes from initial slot content which is trusted\n this.appendChild(loadingContainer);\n break;\n case 'error':\n const errorContainer = document.createElement('div');\n errorContainer.className = 'mu-fetch__error';\n\n // Safe: Create error message safely using textContent\n // We don't use the template string replacement anymore to avoid XSS\n // If user provided a custom error template, we parse it safely or just use default\n\n const errorMessage = this.error?.message || 'Unknown error';\n\n if (this._errorTemplate && this._errorTemplate.includes('${error}')) {\n // If template is complex, we might need a safer way, but for now \n // let's stick to safe text replacement if possible or just use textContent\n // Actually, simply setting textContent into the container is safest for the message\n errorContainer.textContent = `Error: ${errorMessage}`;\n // If we want to respect the slot, we'd need a safe interpolate, \n // but for high, security we break the unsafe template behavior for errors.\n } else {\n const alert = document.createElement('mu-alert');\n alert.setAttribute('variant', 'error');\n alert.textContent = errorMessage; // Safe\n errorContainer.appendChild(alert);\n }\n\n this.appendChild(errorContainer);\n break;\n case 'success':\n if (!this.data || (Array.isArray(this.data) && this.data.length === 0)) {\n const emptyContainer = document.createElement('div');\n emptyContainer.className = 'mu-fetch__empty';\n emptyContainer.innerHTML = this._emptyTemplate; // Trusted slot content\n this.appendChild(emptyContainer);\n } else {\n const dataContainer = document.createElement('div');\n dataContainer.className = 'mu-fetch__data';\n\n // Dataslot or renderItem function\n if (this.renderItem && Array.isArray(this.data)) {\n // WARNING: renderItem expects to return a string currently. \n // To be safe, we should check if it returns a Node or string.\n // For now we will assume string but we can sanitize it? \n // Actually, following the plan, we change default behavior but if user provides function\n // they might output HTML. \n // Let's implement safe node appending if possible.\n this.data.forEach(item => {\n const result = this.renderItem(item);\n if (result instanceof Node) {\n dataContainer.appendChild(result);\n } else {\n // If string, we must be careful. \n // Ideally we use insertAdjacentHTML but that is XSS vector if renderItem is unsafe.\n // We will document that renderItem must be safe.\n // For the PoC, we will show that we can make it safe by NOT using innerHTML on the container directly\n // but appending item by item?\n // innerHTML += result is bad.\n const temp = document.createElement('div');\n temp.innerHTML = result; // Still unsafe if result is bad.\n // We can't fix user's renderItem but we can fix the container\n dataContainer.insertAdjacentHTML('beforeend', result);\n }\n });\n } else {\n const slot = document.createElement('slot');\n slot.name = 'data';\n dataContainer.appendChild(slot);\n this.emit('mu-data', { data: this.data });\n }\n this.appendChild(dataContainer);\n }\n break;\n }\n }\n\n /**\n * Fetch data from the URL\n * @param {RequestInit} [options] - Fetch options override\n * @returns {Promise<any>}\n */\n async fetch(options = {}) {\n const url = this.attr('url', '');\n if (!url) {\n console.warn('mu-fetch: No URL provided');\n return null;\n }\n\n // Abort any in-flight request\n if (this._controller) {\n this._controller.abort();\n }\n this._controller = new AbortController();\n\n this.state = 'loading';\n this.error = null;\n this.render();\n this.emit('mu-loading');\n\n try {\n const method = this.attr('method', 'GET');\n const response = await fetch(url, {\n method,\n signal: this._controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n ...options.headers\n },\n ...options\n });\n\n if (!response.ok) {\n // If the server returns JSON error, try to parse it\n let errorMsg = `HTTP ${response.status}: ${response.statusText}`;\n try {\n const errorJson = await response.json();\n if (errorJson.message) errorMsg = errorJson.message;\n } catch (e) {\n // Ignore json parse error\n }\n throw new Error(errorMsg);\n }\n\n this.data = await response.json();\n this.state = 'success';\n this.render();\n this.emit('mu-success', { data: this.data });\n return this.data;\n } catch (err) {\n if (err.name === 'AbortError') {\n return null; // Aborted, ignore\n }\n this.error = /** @type {Error} */ (err);\n this.state = 'error';\n this.render();\n this.emit('mu-error', { error: this.error });\n return null;\n }\n }\n\n /**\n * Refetch the current URL\n * @returns {Promise<any>}\n */\n refetch() {\n return this.fetch();\n }\n\n /**\n * Set custom render function for array data\n * @param {(item: any, index: number) => string} fn\n */\n set renderItem(fn) {\n this._renderItem = fn;\n }\n\n get renderItem() {\n return this._renderItem;\n }\n\n update(attr, newValue, oldValue) {\n if (attr === 'url' && newValue !== oldValue) {\n this.fetch();\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this._controller) {\n this._controller.abort();\n }\n // Note: setInterval cleanup now handled by MuElement via super.disconnectedCallback()\n }\n}\n\ndefine('mu-fetch', MuFetch);\n",
|
|
66
66
|
"/**\n * mu-datatable.js - Smart Data Table Component\n * \n * AI-First data table with built-in sorting, pagination, and filtering.\n * Designed to minimize boilerplate code when displaying tabular data.\n * \n * @example\n * <mu-datatable\n * columns='[\n * {\"field\": \"name\", \"label\": \"Name\", \"sortable\": true},\n * {\"field\": \"email\", \"label\": \"Email\"},\n * {\"field\": \"role\", \"label\": \"Role\", \"filterable\": true}\n * ]'\n * page-size=\"10\"\n * sortable>\n * </mu-datatable>\n * \n * <script>\n * document.querySelector('mu-datatable').setData(usersArray);\n * </script>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\nimport { debounce } from '../core/render.js';\n\nexport class MuDataTable extends MuElement {\n static get observedAttributes() {\n return ['columns', 'page-size', 'sortable', 'selectable', 'filterable', 'empty-message'];\n }\n\n static baseClass = 'mu-datatable';\n static cssFile = 'datatable';\n\n constructor() {\n super();\n\n // Internal state\n this._data = [];\n this._filteredData = [];\n this._columns = [];\n this._currentPage = 1;\n this._sortField = null;\n this._sortDirection = 'asc';\n this._selectedRows = new Set();\n this._filterValue = '';\n }\n\n // === PROPERTIES ===\n\n get columns() {\n const attr = this.getAttribute('columns');\n if (attr) {\n try {\n return JSON.parse(attr);\n } catch (e) {\n console.warn('[mu-datatable] Invalid columns JSON:', e);\n return [];\n }\n }\n return this._columns;\n }\n\n set columns(value) {\n this._columns = Array.isArray(value) ? value : [];\n if (this.isConnected) this.render();\n }\n\n get pageSize() {\n return parseInt(this.getAttribute('page-size')) || 10;\n }\n\n set pageSize(value) {\n this.setAttribute('page-size', value);\n }\n\n get sortable() {\n return this.hasAttribute('sortable');\n }\n\n get selectable() {\n return this.hasAttribute('selectable');\n }\n\n get filterable() {\n return this.hasAttribute('filterable');\n }\n\n get emptyMessage() {\n return this.getAttribute('empty-message') || 'No data available';\n }\n\n // === LIFECYCLE ===\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n // === PUBLIC API ===\n\n /**\n * Set table data programmatically\n * @param {Array<Object>} data - Array of row objects\n */\n setData(data) {\n this._data = Array.isArray(data) ? data : [];\n this._filteredData = [...this._data];\n this._currentPage = 1;\n this._selectedRows.clear();\n this._applySort();\n this.render();\n\n this.emit('mu-data-change', { data: this._data, count: this._data.length }, { bubbles: true });\n }\n\n /**\n * Get current data\n * @returns {Array<Object>}\n */\n getData() {\n return [...this._data];\n }\n\n /**\n * Get current sort state\n * @returns {{ field: string|null, direction: 'asc'|'desc' }}\n */\n getSortState() {\n return { field: this._sortField, direction: this._sortDirection };\n }\n\n /**\n * Get selected rows\n * @returns {Array<Object>}\n */\n getSelectedRows() {\n return this._data.filter((_, idx) => this._selectedRows.has(idx));\n }\n\n /**\n * Get current page info\n * @returns {{ page: number, pageSize: number, totalPages: number, totalRows: number }}\n */\n getPageInfo() {\n const totalPages = Math.ceil(this._filteredData.length / this.pageSize);\n return {\n page: this._currentPage,\n pageSize: this.pageSize,\n totalPages,\n totalRows: this._filteredData.length\n };\n }\n\n /**\n * Go to specific page\n * @param {number} page\n */\n goToPage(page) {\n const maxPage = Math.ceil(this._filteredData.length / this.pageSize) || 1;\n this._currentPage = Math.max(1, Math.min(page, maxPage));\n this.render();\n\n this.emit('mu-page-change', this.getPageInfo(), { bubbles: true });\n }\n\n /**\n * Sort by field\n * @param {string} field\n * @param {'asc'|'desc'} [direction]\n */\n sortBy(field, direction) {\n if (this._sortField === field && !direction) {\n this._sortDirection = this._sortDirection === 'asc' ? 'desc' : 'asc';\n } else {\n this._sortField = field;\n this._sortDirection = direction || 'asc';\n }\n this._applySort();\n this.render();\n\n this.emit('mu-sort-change', this.getSortState(), { bubbles: true });\n }\n\n /**\n * Filter data\n * @param {string} query\n */\n filter(query) {\n this._filterValue = query.toLowerCase();\n this._applyFilter();\n this._currentPage = 1;\n this.render(false); // Partial render - preserve search input focus\n }\n\n // === INTERNAL METHODS ===\n\n _applySort() {\n if (!this._sortField) return;\n\n this._filteredData.sort((a, b) => {\n const valA = a[this._sortField] ?? '';\n const valB = b[this._sortField] ?? '';\n\n // Handle different types\n if (typeof valA === 'number' && typeof valB === 'number') {\n return this._sortDirection === 'asc' ? valA - valB : valB - valA;\n }\n\n const strA = String(valA).toLowerCase();\n const strB = String(valB).toLowerCase();\n const result = strA.localeCompare(strB);\n return this._sortDirection === 'asc' ? result : -result;\n });\n }\n\n _applyFilter() {\n if (!this._filterValue) {\n this._filteredData = [...this._data];\n return;\n }\n\n const filterableColumns = this.columns.filter(c => c.filterable !== false);\n const fields = filterableColumns.length > 0\n ? filterableColumns.map(c => c.field)\n : this.columns.map(c => c.field);\n\n this._filteredData = this._data.filter(row => {\n return fields.some(field => {\n const value = String(row[field] ?? '').toLowerCase();\n return value.includes(this._filterValue);\n });\n });\n }\n\n _getPageData() {\n const start = (this._currentPage - 1) * this.pageSize;\n const end = start + this.pageSize;\n return this._filteredData.slice(start, end);\n }\n\n _handleHeaderClick(field) {\n const column = this.columns.find(c => c.field === field);\n if (column?.sortable !== false && this.sortable) {\n this.sortBy(field);\n }\n }\n\n _handleRowSelect(index, checked) {\n const actualIndex = (this._currentPage - 1) * this.pageSize + index;\n if (checked) {\n this._selectedRows.add(actualIndex);\n } else {\n this._selectedRows.delete(actualIndex);\n }\n this.render();\n\n this.emit('mu-selection-change', { selected: this.getSelectedRows() }, { bubbles: true });\n }\n\n _handleSelectAll(checked) {\n const pageData = this._getPageData();\n const startIdx = (this._currentPage - 1) * this.pageSize;\n\n pageData.forEach((_, idx) => {\n if (checked) {\n this._selectedRows.add(startIdx + idx);\n } else {\n this._selectedRows.delete(startIdx + idx);\n }\n });\n\n this.render();\n\n this.emit('mu-selection-change', { selected: this.getSelectedRows() }, { bubbles: true });\n }\n\n render(fullRender = true) {\n const columns = this.columns;\n const pageData = this._getPageData();\n const { page, totalPages, totalRows } = this.getPageInfo();\n const startIdx = (this._currentPage - 1) * this.pageSize;\n\n // Check if all current page items are selected\n const allSelected = pageData.length > 0 &&\n pageData.every((_, idx) => this._selectedRows.has(startIdx + idx));\n\n // Surgical update: only update tbody and info if already rendered\n const existingTbody = this.querySelector('.mu-datatable-table tbody');\n const existingInfo = this.querySelector('.mu-datatable-info');\n const existingPagination = this.querySelector('.mu-datatable-pagination');\n\n if (!fullRender && existingTbody) {\n // Update only the parts that changed\n existingTbody.innerHTML = this._renderTbody(pageData, columns, startIdx, allSelected);\n\n if (existingInfo) {\n existingInfo.innerHTML = `${totalRows} row${totalRows !== 1 ? 's' : ''}${this._selectedRows.size > 0 ? ` · ${this._selectedRows.size} selected` : ''}`;\n }\n\n if (existingPagination) {\n existingPagination.innerHTML = this._renderPaginationContent(page, totalPages, totalRows);\n } else if (totalPages > 1) {\n // Need to add pagination\n const wrapper = this.querySelector('.mu-datatable-wrapper');\n if (wrapper) {\n wrapper.insertAdjacentHTML('beforeend', this._renderPagination(page, totalPages, totalRows));\n }\n }\n\n this._attachEventListeners();\n return;\n }\n\n // Full render\n this.innerHTML = `\n <div class=\"mu-datatable-wrapper\">\n ${this.filterable ? `\n <div class=\"mu-datatable-toolbar\">\n <div class=\"mu-datatable-search\">\n <input \n type=\"search\" \n placeholder=\"Search...\" \n value=\"${escapeHTML(this._filterValue)}\"\n >\n </div>\n <div class=\"mu-datatable-info\">\n ${totalRows} row${totalRows !== 1 ? 's' : ''}\n ${this._selectedRows.size > 0 ? ` · ${this._selectedRows.size} selected` : ''}\n </div>\n </div>\n ` : ''}\n\n <table class=\"mu-datatable-table\">\n <thead>\n <tr>\n ${this.selectable ? `\n <th class=\"mu-datatable-checkbox-cell\">\n <input \n type=\"checkbox\" \n ${allSelected ? 'checked' : ''}\n data-select-all\n >\n </th>\n ` : ''}\n ${columns.map(col => {\n const isSortable = col.sortable !== false && this.sortable;\n const isSorted = this._sortField === col.field;\n const sortIcon = isSorted\n ? (this._sortDirection === 'asc' ? '↑' : '↓')\n : '↕';\n return `\n <th \n class=\"${isSortable ? 'mu-datatable-sortable' : ''} ${isSorted ? 'mu-datatable-sorted' : ''}\"\n data-field=\"${escapeHTML(col.field)}\"\n >\n ${escapeHTML(col.label || col.field)}\n ${isSortable ? `<span class=\"mu-datatable-sort-icon\">${sortIcon}</span>` : ''}\n </th>\n `;\n }).join('')}\n </tr>\n </thead>\n <tbody>\n ${this._renderTbody(pageData, columns, startIdx, allSelected)}\n </tbody>\n </table>\n\n ${this._renderPagination(page, totalPages, totalRows)}\n </div>\n `;\n\n this._attachEventListeners();\n }\n\n _renderTbody(pageData, columns, startIdx, allSelected) {\n if (pageData.length === 0) {\n return `\n <tr>\n <td colspan=\"${columns.length + (this.selectable ? 1 : 0)}\" class=\"mu-datatable-empty-state\">\n ${escapeHTML(this.emptyMessage)}\n </td>\n </tr>\n `;\n }\n\n return pageData.map((row, idx) => {\n const actualIdx = startIdx + idx;\n const isSelected = this._selectedRows.has(actualIdx);\n\n return `\n <tr class=\"${isSelected ? 'mu-datatable-selected' : ''}\" data-row-index=\"${idx}\">\n ${this.selectable ? `\n <td class=\"mu-datatable-checkbox-cell\">\n <input \n type=\"checkbox\" \n ${isSelected ? 'checked' : ''}\n data-row-select=\"${idx}\"\n >\n </td>\n ` : ''}\n ${columns.map(col => `\n <td data-field=\"${escapeHTML(col.field)}\">\n ${col.render ? col.render(row[col.field], row) : escapeHTML(row[col.field] ?? '')}\n </td>\n `).join('')}\n </tr>\n `;\n }).join('');\n }\n\n _renderPagination(page, totalPages, totalRows) {\n if (totalPages <= 1) return '';\n return `\n <div class=\"mu-datatable-pagination\">\n ${this._renderPaginationContent(page, totalPages, totalRows)}\n </div>\n `;\n }\n\n _renderPaginationContent(page, totalPages, totalRows) {\n return `\n <div class=\"mu-datatable-pagination-info\">\n Showing ${(page - 1) * this.pageSize + 1}–${Math.min(page * this.pageSize, totalRows)} of ${totalRows}\n </div>\n <div class=\"mu-datatable-pagination-controls\">\n <button class=\"mu-datatable-pagination-btn\" data-page=\"prev\" ${page <= 1 ? 'disabled' : ''}>\n ←\n </button>\n ${this._getPaginationButtons(page, totalPages)}\n <button class=\"mu-datatable-pagination-btn\" data-page=\"next\" ${page >= totalPages ? 'disabled' : ''}>\n →\n </button>\n </div>\n `;\n }\n\n _getPaginationButtons(current, total) {\n const buttons = [];\n const maxButtons = 5;\n\n let start = Math.max(1, current - Math.floor(maxButtons / 2));\n let end = Math.min(total, start + maxButtons - 1);\n\n if (end - start < maxButtons - 1) {\n start = Math.max(1, end - maxButtons + 1);\n }\n\n for (let i = start; i <= end; i++) {\n buttons.push(`\n <button \n class=\"mu-datatable-pagination-btn ${i === current ? 'mu-datatable-pagination-btn-active' : ''}\"\n data-page=\"${i}\"\n >\n ${i}\n </button>\n `);\n }\n\n return buttons.join('');\n }\n\n _attachEventListeners() {\n // Header click for sorting\n this.querySelectorAll('th.mu-datatable-sortable').forEach(th => {\n this.listen(th, 'click', () => {\n this._handleHeaderClick(th.dataset.field);\n });\n });\n\n // Pagination\n this.querySelectorAll('.mu-datatable-pagination-btn[data-page]').forEach(btn => {\n this.listen(btn, 'click', () => {\n const page = btn.dataset.page;\n if (page === 'prev') {\n this.goToPage(this._currentPage - 1);\n } else if (page === 'next') {\n this.goToPage(this._currentPage + 1);\n } else {\n this.goToPage(parseInt(page));\n }\n });\n });\n\n // Row selection\n this.querySelectorAll('[data-row-select]').forEach(cb => {\n this.listen(cb, 'change', (e) => {\n this._handleRowSelect(parseInt(cb.dataset.rowSelect), e.target.checked);\n });\n });\n\n // Select all\n const selectAll = this.querySelector('[data-select-all]');\n if (selectAll) {\n this.listen(selectAll, 'change', (e) => {\n this._handleSelectAll(e.target.checked);\n });\n }\n\n // Search/filter\n const searchInput = this.querySelector('.mu-datatable-search input');\n if (searchInput) {\n const debouncedFilter = debounce((value) => this.filter(value), 300);\n this.listen(searchInput, 'input', (e) => debouncedFilter(e.target.value));\n }\n }\n}\n\nif (!customElements.get('mu-datatable')) {\n customElements.define('mu-datatable', MuDataTable);\n}\n",
|
|
67
|
-
"/**\n * mu-schema-form.js - Auto-Generated Forms from JSON Schema\n * \n * AI-First form component that generates microUI form fields automatically\n * from a JSON Schema definition. Dramatically reduces boilerplate for form creation.\n * \n * @example\n * <mu-schema-form id=\"user-form\"></mu-schema-form>\n * \n * <script>\n * const form = document.getElementById('user-form');\n * form.schema = {\n * type: 'object',\n * properties: {\n * name: { type: 'string', title: 'Full Name', minLength: 2 },\n * email: { type: 'string', format: 'email', title: 'Email' },\n * role: { type: 'string', enum: ['user', 'admin'], title: 'Role' },\n * active: { type: 'boolean', title: 'Active', default: true }\n * },\n * required: ['name', 'email']\n * };\n * \n * form.addEventListener('mu-submit', (e) => {\n * console.log('Form data:', e.detail);\n * });\n * </script>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\n\nexport class MuSchemaForm extends MuElement {\n static get observedAttributes() {\n return ['submit-label', 'reset-label', 'inline', 'readonly', 'disabled'];\n }\n\n static baseClass = 'mu-schema-form';\n static cssFile = 'schema-form';\n\n constructor() {\n super();\n\n this._schema = null;\n this._values = {};\n this._errors = {};\n this._touched = {};\n }\n\n // === PROPERTIES ===\n\n get schema() {\n return this._schema;\n }\n\n set schema(value) {\n this._schema = value;\n this._initializeValues();\n if (this.isConnected) this.render();\n }\n\n get submitLabel() {\n return this.getAttribute('submit-label') || 'Submit';\n }\n\n get resetLabel() {\n return this.getAttribute('reset-label') || 'Reset';\n }\n\n get inline() {\n return this.hasAttribute('inline');\n }\n\n get readonly() {\n return this.hasAttribute('readonly');\n }\n\n get disabled() {\n return this.hasAttribute('disabled');\n }\n\n // === LIFECYCLE ===\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n // === PUBLIC API ===\n\n /**\n * Get current form values\n * @returns {Object}\n */\n getValues() {\n return { ...this._values };\n }\n\n /**\n * Set form values\n * @param {Object} values\n */\n setValues(values) {\n this._values = { ...this._values, ...values };\n this.render();\n }\n\n /**\n * Validate all fields\n * @returns {boolean} True if valid\n */\n validate() {\n if (!this._schema || !this._schema.properties) return true;\n\n this._errors = {};\n const required = this._schema.required || [];\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n const value = this._values[field];\n const errors = this._validateField(field, value, def, required.includes(field));\n if (errors.length > 0) {\n this._errors[field] = errors;\n }\n }\n\n this.render();\n return Object.keys(this._errors).length === 0;\n }\n\n /**\n * Get validation errors\n * @returns {Object}\n */\n getErrors() {\n return { ...this._errors };\n }\n\n /**\n * Reset form to default values\n */\n reset() {\n this._initializeValues();\n this._errors = {};\n this._touched = {};\n this.render();\n\n this.emit('mu-reset', { values: this.getValues() }, { bubbles: true });\n }\n\n /**\n * Submit the form programmatically\n */\n submit() {\n if (this.validate()) {\n this.emit('mu-submit', this.getValues(), { bubbles: true });\n }\n }\n\n // === INTERNAL METHODS ===\n\n _initializeValues() {\n this._values = {};\n if (!this._schema || !this._schema.properties) return;\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n if (def.default !== undefined) {\n this._values[field] = def.default;\n } else if (def.type === 'boolean') {\n this._values[field] = false;\n } else if (def.type === 'number' || def.type === 'integer') {\n this._values[field] = null;\n } else {\n this._values[field] = '';\n }\n }\n }\n\n _validateField(field, value, def, isRequired) {\n const errors = [];\n\n // Required check\n if (isRequired && (value === '' || value === null || value === undefined)) {\n errors.push(`${def.title || field} is required`);\n return errors;\n }\n\n if (value === '' || value === null || value === undefined) return errors;\n\n // Type-specific validation\n if (def.type === 'string') {\n if (def.minLength && value.length < def.minLength) {\n errors.push(`Minimum ${def.minLength} characters`);\n }\n if (def.maxLength && value.length > def.maxLength) {\n errors.push(`Maximum ${def.maxLength} characters`);\n }\n if (def.pattern && !new RegExp(def.pattern).test(value)) {\n errors.push(def.patternError || 'Invalid format');\n }\n if (def.format === 'email' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push('Invalid email address');\n }\n if (def.format === 'url' && !/^https?:\\/\\/.+/.test(value)) {\n errors.push('Invalid URL');\n }\n }\n\n if (def.type === 'number' || def.type === 'integer') {\n const num = Number(value);\n if (isNaN(num)) {\n errors.push('Must be a number');\n } else {\n if (def.minimum !== undefined && num < def.minimum) {\n errors.push(`Minimum value is ${def.minimum}`);\n }\n if (def.maximum !== undefined && num > def.maximum) {\n errors.push(`Maximum value is ${def.maximum}`);\n }\n }\n }\n\n return errors;\n }\n\n _getInputType(def) {\n if (def.format === 'email') return 'email';\n if (def.format === 'password') return 'password';\n if (def.format === 'date') return 'date';\n if (def.format === 'time') return 'time';\n if (def.format === 'datetime-local') return 'datetime-local';\n if (def.format === 'url') return 'url';\n if (def.format === 'tel') return 'tel';\n if (def.type === 'number' || def.type === 'integer') return 'number';\n return 'text';\n }\n\n _renderField(field, def, isRequired) {\n const rawValue = this._values[field] ?? '';\n const errors = this._errors[field] || [];\n const hasError = errors.length > 0;\n\n // XSS Protection: escape all schema-derived content\n const safeField = escapeHTML(field);\n const safeLabel = escapeHTML(def.title || field);\n const safePlaceholder = escapeHTML(def.placeholder || '');\n const safeDescription = escapeHTML(def.description || '');\n const safeValue = escapeHTML(String(rawValue));\n const safeError = hasError ? escapeHTML(errors[0]) : '';\n const disabled = this.disabled || def.readOnly;\n const readonly = this.readonly;\n\n // Boolean → Switch\n if (def.type === 'boolean') {\n return `\n <div class=\"mu-schema-form-field\" data-field=\"${safeField}\">\n <div class=\"mu-schema-form-switch-row\">\n <mu-switch \n ${rawValue ? 'checked' : ''}\n ${disabled ? 'disabled' : ''}\n data-field=\"${safeField}\"\n ></mu-switch>\n <label class=\"mu-schema-form-switch-label\">${safeLabel}</label>\n </div>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n </div>\n `;\n }\n\n // Enum → Dropdown\n if (def.enum) {\n const options = def.enum.map((val, i) => {\n const safeOptValue = escapeHTML(String(val));\n const safeOptLabel = escapeHTML(def.enumLabels?.[i] || String(val));\n return `<mu-option value=\"${safeOptValue}\" ${rawValue === val ? 'selected' : ''}>${safeOptLabel}</mu-option>`;\n }).join('');\n\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-dropdown \n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >\n ${options}\n </mu-dropdown>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Multiline string → Textarea\n if (def.type === 'string' && def.multiline) {\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-textarea\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >${safeValue}</mu-textarea>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Default → Input\n const inputType = this._getInputType(def);\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-input\n type=\"${inputType}\"\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n ${def.minimum !== undefined ? `min=\"${def.minimum}\"` : ''}\n ${def.maximum !== undefined ? `max=\"${def.maximum}\"` : ''}\n ${def.minLength ? `minlength=\"${def.minLength}\"` : ''}\n ${def.maxLength ? `maxlength=\"${def.maxLength}\"` : ''}\n data-field=\"${safeField}\"\n ></mu-input>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n render() {\n if (!this._schema || !this._schema.properties) {\n this.innerHTML = `\n <div class=\"mu-schema-form-empty-state\">\n Set the <code>schema</code> property to generate a form\n </div>\n `;\n return;\n }\n\n const required = this._schema.required || [];\n const fields = Object.entries(this._schema.properties)\n .map(([field, def]) => this._renderField(field, def, required.includes(field)))\n .join('');\n\n this.innerHTML = `\n <div class=\"mu-schema-form ${this.inline ? 'mu-schema-form-inline' : ''}\">\n ${fields}\n \n <div class=\"mu-schema-form-actions\">\n <mu-button variant=\"filled\" data-action=\"submit\" ${this.disabled ? 'disabled' : ''}>\n ${this.submitLabel}\n </mu-button>\n <mu-button variant=\"text\" data-action=\"reset\" ${this.disabled ? 'disabled' : ''}>\n ${this.resetLabel}\n </mu-button>\n </div>\n </div>\n `;\n\n this._attachEventListeners();\n }\n\n _attachEventListeners() {\n // Input changes\n this.querySelectorAll('mu-input').forEach(input => {\n this.listen(input, 'mu-input', (e) => {\n const field = input.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n // Clear error on edit\n if (this._errors[field]) {\n delete this._errors[field];\n input.closest('.mu-schema-form-field').classList.remove('mu-schema-form-field-error');\n input.closest('.mu-schema-form-field').querySelector('.mu-schema-form-error')?.remove();\n }\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Textarea changes\n this.querySelectorAll('mu-textarea').forEach(textarea => {\n this.listen(textarea, 'mu-input', (e) => {\n const field = textarea.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Dropdown changes\n this.querySelectorAll('mu-dropdown').forEach(dropdown => {\n this.listen(dropdown, 'mu-change', (e) => {\n const field = dropdown.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Switch changes\n this.querySelectorAll('mu-switch').forEach(sw => {\n this.listen(sw, 'mu-change', (e) => {\n const field = sw.dataset.field;\n this._values[field] = e.detail.checked;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.checked, values: this.getValues() }, { bubbles: true });\n });\n });\n }\n\n /**\n * Handle actions from MuElement's centralized event delegation\n */\n handleAction(action, target, event) {\n if (action === 'submit') {\n this.submit();\n } else if (action === 'reset') {\n this.reset();\n }\n }\n}\n\nif (!customElements.get('mu-schema-form')) {\n customElements.define('mu-schema-form', MuSchemaForm);\n}\n",
|
|
67
|
+
"/**\n * mu-schema-form.js - Auto-Generated Forms from JSON Schema\n * \n * AI-First form component that generates microUI form fields automatically\n * from a JSON Schema definition. Dramatically reduces boilerplate for form creation.\n * \n * @example\n * <mu-schema-form id=\"user-form\"></mu-schema-form>\n * \n * <script>\n * const form = document.getElementById('user-form');\n * form.schema = {\n * type: 'object',\n * properties: {\n * name: { type: 'string', title: 'Full Name', minLength: 2 },\n * email: { type: 'string', format: 'email', title: 'Email' },\n * role: { type: 'string', enum: ['user', 'admin'], title: 'Role' },\n * active: { type: 'boolean', title: 'Active', default: true }\n * },\n * required: ['name', 'email']\n * };\n * \n * form.addEventListener('mu-submit', (e) => {\n * console.log('Form data:', e.detail); // debug\n * });\n * </script>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\n\nexport class MuSchemaForm extends MuElement {\n static get observedAttributes() {\n return ['submit-label', 'reset-label', 'inline', 'readonly', 'disabled'];\n }\n\n static baseClass = 'mu-schema-form';\n static cssFile = 'schema-form';\n\n constructor() {\n super();\n\n this._schema = null;\n this._values = {};\n this._errors = {};\n this._touched = {};\n }\n\n // === PROPERTIES ===\n\n get schema() {\n return this._schema;\n }\n\n set schema(value) {\n this._schema = value;\n this._initializeValues();\n if (this.isConnected) this.render();\n }\n\n get submitLabel() {\n return this.getAttribute('submit-label') || 'Submit';\n }\n\n get resetLabel() {\n return this.getAttribute('reset-label') || 'Reset';\n }\n\n get inline() {\n return this.hasAttribute('inline');\n }\n\n get readonly() {\n return this.hasAttribute('readonly');\n }\n\n get disabled() {\n return this.hasAttribute('disabled');\n }\n\n // === LIFECYCLE ===\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n // === PUBLIC API ===\n\n /**\n * Get current form values\n * @returns {Object}\n */\n getValues() {\n return { ...this._values };\n }\n\n /**\n * Set form values\n * @param {Object} values\n */\n setValues(values) {\n this._values = { ...this._values, ...values };\n this.render();\n }\n\n /**\n * Validate all fields\n * @returns {boolean} True if valid\n */\n validate() {\n if (!this._schema || !this._schema.properties) return true;\n\n this._errors = {};\n const required = this._schema.required || [];\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n const value = this._values[field];\n const errors = this._validateField(field, value, def, required.includes(field));\n if (errors.length > 0) {\n this._errors[field] = errors;\n }\n }\n\n this.render();\n return Object.keys(this._errors).length === 0;\n }\n\n /**\n * Get validation errors\n * @returns {Object}\n */\n getErrors() {\n return { ...this._errors };\n }\n\n /**\n * Reset form to default values\n */\n reset() {\n this._initializeValues();\n this._errors = {};\n this._touched = {};\n this.render();\n\n this.emit('mu-reset', { values: this.getValues() }, { bubbles: true });\n }\n\n /**\n * Submit the form programmatically\n */\n submit() {\n if (this.validate()) {\n this.emit('mu-submit', this.getValues(), { bubbles: true });\n }\n }\n\n // === INTERNAL METHODS ===\n\n _initializeValues() {\n this._values = {};\n if (!this._schema || !this._schema.properties) return;\n\n for (const [field, def] of Object.entries(this._schema.properties)) {\n if (def.default !== undefined) {\n this._values[field] = def.default;\n } else if (def.type === 'boolean') {\n this._values[field] = false;\n } else if (def.type === 'number' || def.type === 'integer') {\n this._values[field] = null;\n } else {\n this._values[field] = '';\n }\n }\n }\n\n _validateField(field, value, def, isRequired) {\n const errors = [];\n\n // Required check\n if (isRequired && (value === '' || value === null || value === undefined)) {\n errors.push(`${def.title || field} is required`);\n return errors;\n }\n\n if (value === '' || value === null || value === undefined) return errors;\n\n // Type-specific validation\n if (def.type === 'string') {\n if (def.minLength && value.length < def.minLength) {\n errors.push(`Minimum ${def.minLength} characters`);\n }\n if (def.maxLength && value.length > def.maxLength) {\n errors.push(`Maximum ${def.maxLength} characters`);\n }\n if (def.pattern && !new RegExp(def.pattern).test(value)) {\n errors.push(def.patternError || 'Invalid format');\n }\n if (def.format === 'email' && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n errors.push('Invalid email address');\n }\n if (def.format === 'url' && !/^https?:\\/\\/.+/.test(value)) {\n errors.push('Invalid URL');\n }\n }\n\n if (def.type === 'number' || def.type === 'integer') {\n const num = Number(value);\n if (isNaN(num)) {\n errors.push('Must be a number');\n } else {\n if (def.minimum !== undefined && num < def.minimum) {\n errors.push(`Minimum value is ${def.minimum}`);\n }\n if (def.maximum !== undefined && num > def.maximum) {\n errors.push(`Maximum value is ${def.maximum}`);\n }\n }\n }\n\n return errors;\n }\n\n _getInputType(def) {\n if (def.format === 'email') return 'email';\n if (def.format === 'password') return 'password';\n if (def.format === 'date') return 'date';\n if (def.format === 'time') return 'time';\n if (def.format === 'datetime-local') return 'datetime-local';\n if (def.format === 'url') return 'url';\n if (def.format === 'tel') return 'tel';\n if (def.type === 'number' || def.type === 'integer') return 'number';\n return 'text';\n }\n\n _renderField(field, def, isRequired) {\n const rawValue = this._values[field] ?? '';\n const errors = this._errors[field] || [];\n const hasError = errors.length > 0;\n\n // XSS Protection: escape all schema-derived content\n const safeField = escapeHTML(field);\n const safeLabel = escapeHTML(def.title || field);\n const safePlaceholder = escapeHTML(def.placeholder || '');\n const safeDescription = escapeHTML(def.description || '');\n const safeValue = escapeHTML(String(rawValue));\n const safeError = hasError ? escapeHTML(errors[0]) : '';\n const disabled = this.disabled || def.readOnly;\n const readonly = this.readonly;\n\n // Boolean → Switch\n if (def.type === 'boolean') {\n return `\n <div class=\"mu-schema-form-field\" data-field=\"${safeField}\">\n <div class=\"mu-schema-form-switch-row\">\n <mu-switch \n ${rawValue ? 'checked' : ''}\n ${disabled ? 'disabled' : ''}\n data-field=\"${safeField}\"\n ></mu-switch>\n <label class=\"mu-schema-form-switch-label\">${safeLabel}</label>\n </div>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n </div>\n `;\n }\n\n // Enum → Dropdown\n if (def.enum) {\n const options = def.enum.map((val, i) => {\n const safeOptValue = escapeHTML(String(val));\n const safeOptLabel = escapeHTML(def.enumLabels?.[i] || String(val));\n return `<mu-option value=\"${safeOptValue}\" ${rawValue === val ? 'selected' : ''}>${safeOptLabel}</mu-option>`;\n }).join('');\n\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-dropdown \n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >\n ${options}\n </mu-dropdown>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Multiline string → Textarea\n if (def.type === 'string' && def.multiline) {\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-textarea\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n data-field=\"${safeField}\"\n >${safeValue}</mu-textarea>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n // Default → Input\n const inputType = this._getInputType(def);\n return `\n <div class=\"mu-schema-form-field ${hasError ? 'mu-schema-form-field-error' : ''}\" data-field=\"${safeField}\">\n <mu-input\n type=\"${inputType}\"\n label=\"${safeLabel}${isRequired ? ' *' : ''}\"\n placeholder=\"${safePlaceholder}\"\n value=\"${safeValue}\"\n ${disabled ? 'disabled' : ''}\n ${readonly ? 'readonly' : ''}\n ${isRequired ? 'required' : ''}\n ${def.minimum !== undefined ? `min=\"${def.minimum}\"` : ''}\n ${def.maximum !== undefined ? `max=\"${def.maximum}\"` : ''}\n ${def.minLength ? `minlength=\"${def.minLength}\"` : ''}\n ${def.maxLength ? `maxlength=\"${def.maxLength}\"` : ''}\n data-field=\"${safeField}\"\n ></mu-input>\n ${safeDescription ? `<div class=\"mu-schema-form-description\">${safeDescription}</div>` : ''}\n ${hasError ? `<div class=\"mu-schema-form-error\">${safeError}</div>` : ''}\n </div>\n `;\n }\n\n render() {\n if (!this._schema || !this._schema.properties) {\n this.innerHTML = `\n <div class=\"mu-schema-form-empty-state\">\n Set the <code>schema</code> property to generate a form\n </div>\n `;\n return;\n }\n\n const required = this._schema.required || [];\n const fields = Object.entries(this._schema.properties)\n .map(([field, def]) => this._renderField(field, def, required.includes(field)))\n .join('');\n\n this.innerHTML = `\n <div class=\"mu-schema-form ${this.inline ? 'mu-schema-form-inline' : ''}\">\n ${fields}\n \n <div class=\"mu-schema-form-actions\">\n <mu-button variant=\"filled\" data-action=\"submit\" ${this.disabled ? 'disabled' : ''}>\n ${this.submitLabel}\n </mu-button>\n <mu-button variant=\"text\" data-action=\"reset\" ${this.disabled ? 'disabled' : ''}>\n ${this.resetLabel}\n </mu-button>\n </div>\n </div>\n `;\n\n this._attachEventListeners();\n }\n\n _attachEventListeners() {\n // Input changes\n this.querySelectorAll('mu-input').forEach(input => {\n this.listen(input, 'mu-input', (e) => {\n const field = input.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n // Clear error on edit\n if (this._errors[field]) {\n delete this._errors[field];\n input.closest('.mu-schema-form-field').classList.remove('mu-schema-form-field-error');\n input.closest('.mu-schema-form-field').querySelector('.mu-schema-form-error')?.remove();\n }\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Textarea changes\n this.querySelectorAll('mu-textarea').forEach(textarea => {\n this.listen(textarea, 'mu-input', (e) => {\n const field = textarea.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Dropdown changes\n this.querySelectorAll('mu-dropdown').forEach(dropdown => {\n this.listen(dropdown, 'mu-change', (e) => {\n const field = dropdown.dataset.field;\n this._values[field] = e.detail.value;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.value, values: this.getValues() }, { bubbles: true });\n });\n });\n\n // Switch changes\n this.querySelectorAll('mu-switch').forEach(sw => {\n this.listen(sw, 'mu-change', (e) => {\n const field = sw.dataset.field;\n this._values[field] = e.detail.checked;\n this._touched[field] = true;\n\n this.emit('mu-change', { field, value: e.detail.checked, values: this.getValues() }, { bubbles: true });\n });\n });\n }\n\n /**\n * Handle actions from MuElement's centralized event delegation\n */\n handleAction(action, target, event) {\n if (action === 'submit') {\n this.submit();\n } else if (action === 'reset') {\n this.reset();\n }\n }\n}\n\nif (!customElements.get('mu-schema-form')) {\n customElements.define('mu-schema-form', MuSchemaForm);\n}\n",
|
|
68
68
|
"/**\n * mu-prompt-ui.js - AI Agent Prompt UI Components\n * \n * Specialized components for AI agent interfaces:\n * - mu-prompt-input: Enhanced text input with submit button and loading states\n * - mu-code-block: Syntax-highlighted code display with copy button\n * - mu-agent-toolbar: Action buttons for AI responses (copy, regenerate, etc.)\n * - mu-message-bubble: Chat message bubbles with role-based styling\n * \n * @example\n * <mu-prompt-input placeholder=\"Ask anything...\" loading></mu-prompt-input>\n * \n * <mu-message-bubble role=\"user\">\n * How do I sort an array?\n * </mu-message-bubble>\n * \n * <mu-message-bubble role=\"assistant\">\n * Here's how to sort an array:\n * <mu-code-block language=\"javascript\">\n * const sorted = array.sort((a, b) => a - b);\n * </mu-code-block>\n * <mu-agent-toolbar></mu-agent-toolbar>\n * </mu-message-bubble>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\nimport { escapeHTML } from '../core/utils.js';\n\n// ============================================\n// MU-PROMPT-INPUT\n// ============================================\nexport class MuPromptInput extends MuElement {\n static get observedAttributes() {\n return ['placeholder', 'loading', 'disabled', 'multiline'];\n }\n\n static baseClass = 'mu-prompt-input';\n static cssFile = 'prompt-ui';\n\n constructor() {\n super();\n this._value = '';\n }\n\n get placeholder() {\n return this.getAttribute('placeholder') || 'Type a message...';\n }\n\n get loading() {\n return this.hasAttribute('loading');\n }\n\n set loading(val) {\n if (val) this.setAttribute('loading', '');\n else this.removeAttribute('loading');\n }\n\n get disabled() {\n return this.hasAttribute('disabled');\n }\n\n get multiline() {\n return this.hasAttribute('multiline');\n }\n\n get value() {\n return this._value;\n }\n\n set value(val) {\n this._value = val;\n const input = this.querySelector('.mu-prompt-input-field');\n if (input) input.value = val;\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n _submit() {\n if (this.loading || this.disabled || !this._value.trim()) return;\n\n this.dispatchEvent(new CustomEvent('mu-submit', {\n bubbles: true,\n detail: { value: this._value.trim() }\n }));\n }\n\n _handleKeydown(e) {\n if (e.key === 'Enter' && !e.shiftKey && !this.multiline) {\n e.preventDefault();\n this._submit();\n } else if (e.key === 'Enter' && e.metaKey) {\n e.preventDefault();\n this._submit();\n }\n }\n\n render() {\n const isTextarea = this.multiline;\n\n this.innerHTML = `\n <div class=\"mu-prompt-input-container ${this.loading ? 'mu-prompt-input-loading' : ''}\">\n ${isTextarea\n ? `<textarea class=\"mu-prompt-input-field\" placeholder=\"${this.placeholder}\" ${this.disabled ? 'disabled' : ''}></textarea>`\n : `<input class=\"mu-prompt-input-field\" type=\"text\" placeholder=\"${this.placeholder}\" ${this.disabled ? 'disabled' : ''} />`\n }\n <button class=\"mu-prompt-input-submit\" ${this.disabled || this.loading ? 'disabled' : ''}>\n ${this.loading\n ? '<div class=\"mu-prompt-input-spinner\"></div>'\n : '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2.01 21L23 12 2.01 3 2 10l15 2-15 2z\"/></svg>'\n }\n </button>\n </div>\n `;\n\n const input = this.querySelector('.mu-prompt-input-field');\n const submitBtn = this.querySelector('.mu-prompt-input-submit');\n\n input.value = this._value;\n this.listen(input, 'input', (e) => {\n this._value = e.target.value;\n this.dispatchEvent(new CustomEvent('mu-input', {\n bubbles: true,\n detail: { value: this._value }\n }));\n });\n this.listen(input, 'keydown', (e) => this._handleKeydown(e));\n this.listen(submitBtn, 'click', () => this._submit());\n }\n}\n\n// ============================================\n// MU-CODE-BLOCK\n// ============================================\nexport class MuCodeBlock extends MuElement {\n static get observedAttributes() {\n return ['language', 'filename'];\n }\n\n static baseClass = 'mu-code-block';\n\n constructor() {\n super();\n }\n\n get language() {\n return this.getAttribute('language') || 'text';\n }\n\n get filename() {\n return this.getAttribute('filename');\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n async _copy() {\n const code = this.textContent.trim();\n try {\n await navigator.clipboard.writeText(code);\n const btn = this.querySelector('.mu-code-block-copy-btn');\n btn.textContent = '✓ Copied';\n this.setTimeout(() => {\n btn.textContent = 'Copy';\n }, 2000);\n } catch (e) {\n console.error('Failed to copy:', e);\n }\n }\n\n render() {\n const code = this.textContent.trim();\n\n this.innerHTML = `\n <div class=\"mu-code-block-container\">\n <div class=\"mu-code-block-header\">\n ${this.filename\n ? `<span class=\"mu-code-block-filename\">${this.filename}</span>`\n : `<span class=\"mu-code-block-language\">${this.language}</span>`\n }\n <button class=\"mu-code-block-copy-btn\">Copy</button>\n </div>\n <pre class=\"mu-code-block-pre\"><code>${escapeHTML(code)}</code></pre>\n </div>\n `;\n\n this.listen(this.querySelector('.mu-code-block-copy-btn'), 'click', () => this._copy());\n }\n\n // Using centralized escapeHTML from core/utils.js for XSS safety\n}\n\n// ============================================\n// MU-AGENT-TOOLBAR\n// ============================================\nexport class MuAgentToolbar extends MuElement {\n static get observedAttributes() {\n return ['actions'];\n }\n\n static baseClass = 'mu-agent-toolbar';\n\n constructor() {\n super();\n }\n\n get actions() {\n const attr = this.getAttribute('actions');\n return attr ? attr.split(',').map(a => a.trim()) : ['copy', 'regenerate', 'thumbsUp', 'thumbsDown'];\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.render();\n }\n\n\n\n render() {\n const actionButtons = {\n copy: { icon: '📋', label: 'Copy' },\n regenerate: { icon: '🔄', label: 'Regenerate' },\n thumbsUp: { icon: '👍', label: 'Good response' },\n thumbsDown: { icon: '👎', label: 'Poor response' },\n edit: { icon: '✏️', label: 'Edit' },\n share: { icon: '↗️', label: 'Share' }\n };\n\n const buttons = this.actions\n .filter(a => actionButtons[a])\n .map(a => `\n <button class=\"mu-agent-toolbar-btn\" data-action=\"${a}\" title=\"${actionButtons[a].label}\">\n ${actionButtons[a].icon}\n </button>\n `).join('');\n\n this.innerHTML = buttons;\n }\n\n /**\n * Handle actions from MuElement's centralized event delegation\n */\n handleAction(action, target, event) {\n this.dispatchEvent(new CustomEvent('mu-action', {\n bubbles: true,\n detail: { action }\n }));\n }\n}\n\n// ============================================\n// MU-MESSAGE-BUBBLE\n// ============================================\nexport class MuMessageBubble extends MuElement {\n static get observedAttributes() {\n return ['role', 'avatar', 'name', 'timestamp'];\n }\n\n static baseClass = 'mu-message-bubble';\n\n constructor() {\n super();\n this._originalContent = null;\n }\n\n get role() {\n return this.getAttribute('role') || 'user';\n }\n\n get avatar() {\n return this.getAttribute('avatar');\n }\n\n get name() {\n return this.getAttribute('name') || (this.role === 'user' ? 'You' : 'Assistant');\n }\n\n get timestamp() {\n return this.getAttribute('timestamp');\n }\n\n connectedCallback() {\n super.connectedCallback();\n // Capture original children before render\n if (!this._originalContent) {\n this._originalContent = Array.from(this.childNodes);\n }\n this.render();\n }\n\n attributeChangedCallback(name, oldVal, newVal) {\n if (this.isConnected && oldVal !== newVal) {\n this.render();\n }\n }\n\n render() {\n const isUser = this.role === 'user';\n const defaultAvatar = isUser ? '👤' : '🤖';\n const roleClass = isUser ? 'mu-message-bubble-user' : 'mu-message-bubble-assistant';\n\n // Create structure\n const message = document.createElement('div');\n message.className = `mu-message-bubble-message ${roleClass}`;\n\n // Avatar\n const avatar = document.createElement('div');\n avatar.className = `mu-message-bubble-avatar ${roleClass}`;\n if (this.avatar) {\n const img = document.createElement('img');\n img.src = this.avatar;\n img.alt = this.name;\n avatar.appendChild(img);\n } else {\n avatar.textContent = defaultAvatar;\n }\n message.appendChild(avatar);\n\n // Content wrapper\n const content = document.createElement('div');\n content.className = 'mu-message-bubble-content';\n\n // Header\n const header = document.createElement('div');\n header.className = `mu-message-bubble-header ${roleClass}`;\n const nameSpan = document.createElement('span');\n nameSpan.className = 'mu-message-bubble-name';\n nameSpan.textContent = this.name;\n header.appendChild(nameSpan);\n if (this.timestamp) {\n const timestampSpan = document.createElement('span');\n timestampSpan.className = 'mu-message-bubble-timestamp';\n timestampSpan.textContent = this.timestamp;\n header.appendChild(timestampSpan);\n }\n content.appendChild(header);\n\n // Bubble with original content\n const bubble = document.createElement('div');\n bubble.className = `mu-message-bubble-bubble ${roleClass}`;\n if (this._originalContent) {\n this._originalContent.forEach(child => bubble.appendChild(child.cloneNode(true)));\n }\n content.appendChild(bubble);\n\n message.appendChild(content);\n\n // Set HTML\n this.innerHTML = '';\n this.appendChild(message);\n }\n}\n\n// Register all components\nif (!customElements.get('mu-prompt-input')) {\n customElements.define('mu-prompt-input', MuPromptInput);\n}\nif (!customElements.get('mu-code-block')) {\n customElements.define('mu-code-block', MuCodeBlock);\n}\nif (!customElements.get('mu-agent-toolbar')) {\n customElements.define('mu-agent-toolbar', MuAgentToolbar);\n}\nif (!customElements.get('mu-message-bubble')) {\n customElements.define('mu-message-bubble', MuMessageBubble);\n}\n",
|
|
69
69
|
"/**\n * @fileoverview mu-error-boundary - Error Boundary Component\n * \n * Catches rendering errors in child components and displays fallback UI.\n * Critical for large-scale applications where one component error\n * should not crash the entire application.\n * \n * Based on 2024 enterprise error recovery patterns.\n * \n * Usage:\n * <mu-error-boundary fallback=\"<p>Something went wrong</p>\">\n * <mu-complex-widget></mu-complex-widget>\n * </mu-error-boundary>\n * \n * With error handler:\n * <mu-error-boundary onerror=\"customHandler(event.detail)\">\n * <mu-data-table :items=\"${data}\"></mu-data-table>\n * </mu-error-boundary>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\n\n/**\n * Global error registry for agent observability\n * @type {Array<{timestamp: number, component: string, error: Error, context: Object}>}\n */\nconst errorRegistry = [];\n\n/**\n * Get all caught errors for agent debugging\n * @returns {Array<Object>}\n */\nexport function getErrors() {\n return [...errorRegistry];\n}\n\n/**\n * Clear error registry\n */\nexport function clearErrors() {\n errorRegistry.length = 0;\n}\n\nexport class MuErrorBoundary extends MuElement {\n static baseClass = 'mu-error-boundary';\n static observedAttributes = ['fallback'];\n\n #hasError = false;\n #error = null;\n #originalContent = '';\n\n connectedCallback() {\n super.connectedCallback();\n\n // Store original content for potential recovery\n this.#originalContent = this.innerHTML;\n\n // Listen for errors from child components\n this.listen(this, 'error', this.#handleError.bind(this));\n\n // Also catch unhandled errors from child scripts\n this.#setupGlobalErrorHandler();\n }\n\n #setupGlobalErrorHandler() {\n // Capture errors that occur within this boundary\n const originalHandler = window.onerror;\n\n window.onerror = (message, source, lineno, colno, error) => {\n // Check if error originated from within this boundary\n if (this.#isErrorFromChild(error)) {\n this.#handleError({ detail: { error, message } });\n return true; // Prevent propagation\n }\n\n // Call original handler if exists\n if (originalHandler) {\n return originalHandler(message, source, lineno, colno, error);\n }\n return false;\n };\n }\n\n #isErrorFromChild(error) {\n // Best-effort check if error is from child component\n if (!error?.stack) return false;\n\n const childTags = Array.from(this.querySelectorAll('*'))\n .filter(el => el.tagName.startsWith('MU-'))\n .map(el => el.tagName.toLowerCase());\n\n return childTags.some(tag => error.stack.includes(tag));\n }\n\n #handleError(event) {\n const error = event?.detail?.error || event?.error || event;\n\n if (this.#hasError) return; // Already showing fallback\n\n this.#hasError = true;\n this.#error = error;\n\n // Register error for agent observability\n errorRegistry.push({\n timestamp: Date.now(),\n component: this.id || 'mu-error-boundary',\n error: {\n message: error?.message || String(error),\n stack: error?.stack,\n name: error?.name\n },\n context: {\n originalContent: this.#originalContent.slice(0, 200),\n parentUrl: window.location.href\n }\n });\n\n // Keep only last 50 errors\n if (errorRegistry.length > 50) {\n errorRegistry.shift();\n }\n\n // Show fallback UI\n this.#renderFallback();\n\n // Emit event for custom handling\n this.emit('mu-error', {\n error,\n boundary: this,\n recover: () => this.recover()\n });\n\n // Prevent event propagation\n event?.stopPropagation?.();\n }\n\n #renderFallback() {\n const fallback = this.getAttribute('fallback');\n\n if (fallback) {\n this.innerHTML = fallback;\n } else {\n // Default fallback UI\n this.innerHTML = `\n <div class=\"mu-error-boundary__fallback\" role=\"alert\" aria-live=\"polite\">\n <mu-icon name=\"error\" style=\"--mu-icon-size: 48px; color: var(--md-sys-color-error);\"></mu-icon>\n <p class=\"mu-error-boundary__title\">Something went wrong</p>\n <p class=\"mu-error-boundary__message\">${this.#error?.message || 'An unexpected error occurred'}</p>\n <mu-button variant=\"outlined\" onclick=\"this.closest('mu-error-boundary').recover()\">\n Try Again\n </mu-button>\n </div>\n `;\n }\n\n this.classList.add('has-error');\n }\n\n render() {\n // Apply base styles\n this.style.display = 'block';\n }\n\n /**\n * Attempt to recover by restoring original content\n */\n recover() {\n this.#hasError = false;\n this.#error = null;\n this.classList.remove('has-error');\n this.innerHTML = this.#originalContent;\n\n this.emit('mu-recover');\n }\n\n /**\n * Check if boundary is currently showing error\n */\n get hasError() {\n return this.#hasError;\n }\n\n /**\n * Get the current error\n */\n get error() {\n return this.#error;\n }\n}\n\ndefine('mu-error-boundary', MuErrorBoundary);\n",
|
|
70
70
|
"/**\n * @fileoverview mu-code - Code Block Component with Syntax Highlighting\n * \n * Usage: <mu-code language=\"html\">...</mu-code>\n */\n\nimport { MuElement, define } from '../core/MuElement.js';\n\nexport class MuCode extends MuElement {\n static baseClass = 'mu-code';\n static observedAttributes = ['language'];\n\n #originalCode = null;\n\n connectedCallback() {\n super.connectedCallback();\n // Store original code before render\n if (!this.#originalCode && !this.querySelector('.mu-code__content')) {\n this.#originalCode = this.textContent;\n }\n }\n\n render() {\n // Idempotent: skip if already rendered\n if (this.querySelector('.mu-code__content')) {\n return;\n }\n\n const language = this.attr('language', 'html');\n\n // Get raw code - use innerHTML to preserve any pre-escaped entities\n // textContent would decode < to < which we don't want\n const rawCode = this.#originalCode || this.innerHTML;\n\n // First decode any HTML entities that might already be in the content\n // This handles cases where the HTML source uses < > etc.\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = rawCode;\n const decodedCode = tempDiv.textContent;\n\n // Dedent: remove common leading whitespace from all lines\n const dedentedCode = this.#dedent(decodedCode);\n\n // Auto-indent: apply proper code formatting based on language\n const formattedCode = this.#autoIndent(dedentedCode, language);\n\n // Store formatted version for copy button\n const copyableCode = formattedCode;\n\n // Now escape for display - only &, <, > need escaping for code\n // Do NOT escape quotes as they are safe inside <code> elements\n const escaped = formattedCode\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n\n // Basic syntax highlighting\n const highlighted = this.#highlight(escaped, language);\n\n this.innerHTML = `\n <div class=\"mu-code__header\">\n <span class=\"mu-code__language\">${language.toUpperCase()}</span>\n <button class=\"mu-code__copy\" title=\"Copy code\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/>\n <path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/>\n </svg>\n </button>\n </div>\n <pre class=\"mu-code__pre\"><code class=\"mu-code__content\">${highlighted}</code></pre>\n `;\n\n this.#applyStyles();\n this.#setupCopyButton(copyableCode);\n }\n\n #highlight(code, language) {\n // Shared marker helper to avoid regex self-matching\n // IMPORTANT: Uses Unicode Private Use Area characters + letters to avoid\n // collision with number regex which matches \\b\\d+\\b\n const createHighlighter = () => {\n const markers = [];\n let markerIndex = 0;\n\n const addMarker = (content, className) => {\n // Create a truly unique marker that no highlighting regex can match:\n // Encode the index as a single Unicode PUA character (offset by 0xE100)\n // This way there are NO digits or word characters in the marker at all\n const idx = markerIndex++;\n const markerChar = String.fromCharCode(0xE100 + idx);\n const start = String.fromCharCode(0xE000);\n const end = String.fromCharCode(0xE001);\n const marker = start + markerChar + end;\n markers.push({ marker, content, className });\n return marker;\n };\n\n const resolveMarkers = (result) => {\n for (const { marker, content, className } of markers) {\n result = result.replace(marker, `<span class=\"${className}\">${content}</span>`);\n }\n return result;\n };\n\n return { addMarker, resolveMarkers };\n };\n\n // HTML highlighting\n if (language === 'html' || language === 'xml') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // HTML comments: <!-- ... -->\n result = result.replace(/(<!--[\\s\\S]*?-->)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // DOCTYPE\n result = result.replace(/(<!DOCTYPE[^&]*>)/gi, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Strings (attribute values)\n result = result.replace(/(\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Attributes (name= pattern)\n result = result.replace(/(\\s)([\\w-]+)(=)/g, (match, space, attr, eq) =>\n `${space}${addMarker(attr, 'mu-code__attr')}${eq}`);\n\n // Tags\n result = result.replace(/(<\\/?)([\\w-]+)/g, (match, bracket, tag) =>\n `${bracket}${addMarker(tag, 'mu-code__tag')}`);\n\n return resolveMarkers(result);\n }\n\n // JavaScript highlighting\n if (language === 'javascript' || language === 'js' || language === 'typescript' || language === 'ts') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Multi-line comments /* ... */\n result = result.replace(/(\\/\\*[\\s\\S]*?\\*\\/)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Single-line comments\n result = result.replace(/(\\/\\/.*)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Template literals (backticks)\n result = result.replace(/(`[^`]*`)/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Strings\n result = result.replace(/('[^']*'|\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Numbers (including decimals and scientific notation)\n result = result.replace(/\\b(\\d+\\.?\\d*(?:e[+-]?\\d+)?)\\b/gi, (match) =>\n addMarker(match, 'mu-code__number'));\n\n // Keywords (expanded list)\n result = result.replace(/\\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|class|extends|import|export|from|default|async|await|new|this|super|typeof|instanceof|in|of|try|catch|finally|throw|yield|static|get|set)\\b/g, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Boolean/null/undefined\n result = result.replace(/\\b(true|false|null|undefined|NaN|Infinity)\\b/g, (match) =>\n addMarker(match, 'mu-code__builtin'));\n\n // Function names (word followed by parenthesis)\n result = result.replace(/\\b([a-zA-Z_]\\w*)\\s*(?=\\()/g, (match, name) =>\n addMarker(name, 'mu-code__function'));\n\n return resolveMarkers(result);\n }\n\n // CSS highlighting\n if (language === 'css' || language === 'scss' || language === 'sass') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Comments\n result = result.replace(/(\\/\\*[\\s\\S]*?\\*\\/)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Strings\n result = result.replace(/('[^']*'|\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // At-rules (@media, @keyframes, etc.)\n result = result.replace(/(@[\\w-]+)/g, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Property values with units\n result = result.replace(/\\b(\\d+\\.?\\d*)(px|em|rem|%|vh|vw|s|ms|deg|fr)\\b/g, (match, num, unit) =>\n `${addMarker(num, 'mu-code__number')}${addMarker(unit, 'mu-code__builtin')}`);\n\n // Numbers without units\n result = result.replace(/\\b(\\d+\\.?\\d*)\\b/g, (match) =>\n addMarker(match, 'mu-code__number'));\n\n // Hex colors\n result = result.replace(/(#[0-9a-fA-F]{3,8})\\b/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // CSS properties (word followed by colon)\n result = result.replace(/\\b([\\w-]+)\\s*:/g, (match, prop) =>\n `${addMarker(prop, 'mu-code__attr')}:`);\n\n // Selectors (. # or element at start of line or after comma/brace)\n result = result.replace(/(^|[{,\\s])([.#]?[\\w-]+)(?=\\s*[{,])/gm, (match, before, selector) =>\n `${before}${addMarker(selector, 'mu-code__tag')}`);\n\n return resolveMarkers(result);\n }\n\n // JSON highlighting\n if (language === 'json') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Strings (keys will be handled separately)\n result = result.replace(/(\"[^\"]*\")\\s*:/g, (match, key) =>\n `${addMarker(key, 'mu-code__attr')}:`);\n\n // String values\n result = result.replace(/:\\s*(\"[^\"]*\")/g, (match, val) =>\n `: ${addMarker(val, 'mu-code__string')}`);\n\n // Numbers\n result = result.replace(/:\\s*(-?\\d+\\.?\\d*)/g, (match, num) =>\n `: ${addMarker(num, 'mu-code__number')}`);\n\n // Booleans and null\n result = result.replace(/\\b(true|false|null)\\b/g, (match) =>\n addMarker(match, 'mu-code__builtin'));\n\n return resolveMarkers(result);\n }\n\n // Shell/Bash highlighting\n if (language === 'bash' || language === 'sh' || language === 'shell') {\n const { addMarker, resolveMarkers } = createHighlighter();\n let result = code;\n\n // Comments\n result = result.replace(/(#.*)/g, (match) =>\n addMarker(match, 'mu-code__comment'));\n\n // Strings\n result = result.replace(/('[^']*'|\"[^\"]*\")/g, (match) =>\n addMarker(match, 'mu-code__string'));\n\n // Variables $VAR and ${VAR}\n result = result.replace(/(\\$\\{?\\w+\\}?)/g, (match) =>\n addMarker(match, 'mu-code__attr'));\n\n // Common commands\n result = result.replace(/\\b(cd|ls|echo|cat|grep|find|sudo|npm|npx|bun|node|git|curl|wget|mkdir|rm|cp|mv|chmod|chown)\\b/g, (match) =>\n addMarker(match, 'mu-code__keyword'));\n\n // Flags\n result = result.replace(/(\\s)(--?[\\w-]+)/g, (match, space, flag) =>\n `${space}${addMarker(flag, 'mu-code__builtin')}`);\n\n return resolveMarkers(result);\n }\n\n // No highlighting for unknown languages\n return code;\n }\n\n /**\n * SOTA Dedent - Remove common leading whitespace from all lines\n * Based on TC39 String.dedent proposal and npm 'dedent' package algorithm\n * \n * Algorithm:\n * 1. Split into lines, trim leading/trailing empty lines\n * 2. Find minimum indent across ALL non-empty lines\n * 3. Remove exactly that amount from start of each line\n * \n * This preserves relative indentation within the code.\n */\n #dedent(code) {\n // Split into lines\n let lines = code.split('\\n');\n\n // Remove leading empty lines\n while (lines.length > 0 && lines[0].trim() === '') {\n lines.shift();\n }\n // Remove trailing empty lines\n while (lines.length > 0 && lines[lines.length - 1].trim() === '') {\n lines.pop();\n }\n\n if (lines.length === 0) return '';\n\n // Find minimum indent across ALL non-empty lines\n let minIndent = Infinity;\n for (const line of lines) {\n // Skip empty lines when calculating min indent\n if (line.trim() === '') continue;\n\n const match = line.match(/^(\\s*)/);\n const indent = match ? match[1].length : 0;\n minIndent = Math.min(minIndent, indent);\n }\n\n // If no common indent found, return as-is\n if (minIndent === Infinity || minIndent === 0) {\n return lines.join('\\n');\n }\n\n // Remove exactly minIndent spaces from start of each line\n return lines.map(line => {\n if (line.trim() === '') return ''; // Keep empty lines as empty\n return line.slice(minIndent);\n }).join('\\n');\n }\n\n /**\n * SOTA Auto-Indent - Lightweight bracket-counting code formatter\n * Inspired by js-beautify but minimal (~30 lines vs ~50KB)\n * \n * Supports: JavaScript/JSON ({} blocks), HTML (<> tags), CSS ({} rules)\n * \n * Algorithm:\n * 1. Strip existing indentation (each line trimmed)\n * 2. Track indent level based on opening/closing brackets\n * 3. Apply consistent 4-space indentation\n */\n #autoIndent(code, language) {\n const INDENT = ' '; // 4 spaces\n const lines = code.split('\\n');\n const result = [];\n let indentLevel = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n\n // Skip empty lines\n if (line === '') {\n result.push('');\n continue;\n }\n\n // Calculate bracket changes for this line\n let openBrackets = 0;\n let closeBrackets = 0;\n\n if (language === 'html') {\n // HTML: count opening/closing tags\n // Opening: <tag> or <tag attr>\n // Closing: </tag> or self-closing />\n const openTags = (line.match(/<[a-zA-Z][^>]*(?<!\\/)\\s*>/g) || []).length;\n const closeTags = (line.match(/<\\/[^>]+>/g) || []).length;\n const selfClosing = (line.match(/<[^>]+\\/>/g) || []).length;\n openBrackets = openTags - selfClosing;\n closeBrackets = closeTags;\n } else {\n // JS/CSS/JSON: count { and }\n // Avoid counting brackets inside strings (basic heuristic)\n let inString = false;\n let stringChar = '';\n for (let j = 0; j < line.length; j++) {\n const char = line[j];\n const prev = j > 0 ? line[j - 1] : '';\n\n if ((char === '\"' || char === \"'\" || char === '`') && prev !== '\\\\') {\n if (!inString) {\n inString = true;\n stringChar = char;\n } else if (char === stringChar) {\n inString = false;\n }\n }\n\n if (!inString) {\n if (char === '{' || char === '[' || char === '(') openBrackets++;\n if (char === '}' || char === ']' || char === ')') closeBrackets++;\n }\n }\n }\n\n // Determine if line starts with closing bracket (decrease indent BEFORE this line)\n const startsWithClose = language === 'html'\n ? line.startsWith('</')\n : /^[}\\]\\)]/.test(line);\n\n // Apply closing brackets that start the line BEFORE indenting\n if (startsWithClose && closeBrackets > 0) {\n indentLevel = Math.max(0, indentLevel - 1);\n closeBrackets--; // Already accounted for\n }\n\n // Add the indented line\n result.push(INDENT.repeat(indentLevel) + line);\n\n // Update indent level for next line\n // Net change: opens increase, closes decrease\n indentLevel = Math.max(0, indentLevel + openBrackets - closeBrackets);\n }\n\n return result.join('\\n');\n }\n\n #applyStyles() {\n this.style.display = 'block';\n this.style.background = 'var(--md-sys-color-surface-container-highest)';\n\n // Skip border-radius when embedded in mu-example for seamless integration\n const isEmbedded = this.closest('mu-example');\n if (isEmbedded) {\n // Only top corners flat to align with header, bottom stays rounded\n this.style.borderTopLeftRadius = '0';\n this.style.borderTopRightRadius = '0';\n this.style.borderBottomLeftRadius = 'var(--md-sys-shape-corner-medium)';\n this.style.borderBottomRightRadius = 'var(--md-sys-shape-corner-medium)';\n } else {\n this.style.borderRadius = 'var(--md-sys-shape-corner-medium)';\n }\n\n this.style.overflow = 'hidden';\n this.style.fontFamily = \"'Fira Code', 'Consolas', monospace\";\n this.style.fontSize = '13px';\n\n const header = this.querySelector('.mu-code__header');\n if (header) {\n header.style.display = 'flex';\n header.style.justifyContent = 'space-between';\n header.style.alignItems = 'center';\n header.style.padding = '8px 16px';\n header.style.background = 'var(--md-sys-color-surface-container)';\n header.style.borderBottom = '1px solid var(--md-sys-color-outline-variant)';\n }\n\n const lang = this.querySelector('.mu-code__language');\n if (lang) {\n lang.style.fontSize = '11px';\n lang.style.fontWeight = '600';\n lang.style.color = 'var(--md-sys-color-primary)';\n lang.style.letterSpacing = '0.5px';\n }\n\n const copyBtn = this.querySelector('.mu-code__copy');\n if (copyBtn) {\n copyBtn.style.background = 'transparent';\n copyBtn.style.border = 'none';\n copyBtn.style.cursor = 'pointer';\n copyBtn.style.color = 'var(--md-sys-color-on-surface-variant)';\n copyBtn.style.padding = '4px';\n copyBtn.style.borderRadius = '4px';\n copyBtn.style.display = 'flex';\n copyBtn.style.transition = 'all 0.2s ease';\n }\n\n const pre = this.querySelector('.mu-code__pre');\n if (pre) {\n pre.style.margin = '0';\n pre.style.padding = '16px';\n pre.style.overflow = 'auto';\n pre.style.maxHeight = '400px';\n }\n\n const content = this.querySelector('.mu-code__content');\n if (content) {\n content.style.color = 'var(--md-sys-color-on-surface)';\n }\n\n // Syntax highlighting colors\n const style = document.createElement('style');\n style.textContent = `\n .mu-code__tag { color: var(--md-sys-color-primary); }\n .mu-code__attr { color: var(--md-sys-color-tertiary); }\n .mu-code__string { color: var(--md-sys-color-secondary); }\n .mu-code__keyword { color: var(--md-sys-color-primary); font-weight: 500; }\n .mu-code__comment { color: var(--md-sys-color-outline); font-style: italic; }\n .mu-code__number { color: var(--md-sys-color-error); }\n .mu-code__function { color: var(--md-sys-color-tertiary); }\n .mu-code__builtin { color: var(--md-sys-color-primary); font-weight: 500; }\n .mu-code__copy:hover { \n background: var(--md-sys-color-primary-container) !important;\n color: var(--md-sys-color-on-primary-container) !important;\n }\n .mu-code__copy:active {\n background: var(--md-sys-color-primary) !important;\n color: var(--md-sys-color-on-primary) !important;\n transform: scale(0.95);\n }\n .mu-code__copy.copied {\n background: #c8e6c9 !important;\n color: #1b5e20 !important;\n }\n `;\n // Remove old styles and add new ones to ensure latest CSS\n const existingStyle = document.getElementById('mu-code-styles');\n if (existingStyle) {\n existingStyle.remove();\n }\n style.id = 'mu-code-styles';\n document.head.appendChild(style);\n }\n\n #setupCopyButton(originalCode) {\n const btn = this.querySelector('.mu-code__copy');\n if (btn) {\n this.listen(btn, 'click', async () => {\n try {\n await navigator.clipboard.writeText(originalCode.trim());\n // Add copied class for visual feedback\n btn.classList.add('copied');\n btn.innerHTML = `\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"20 6 9 17 4 12\"/>\n </svg>\n `;\n this.setTimeout(() => {\n btn.classList.remove('copied');\n btn.innerHTML = `\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/>\n <path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/>\n </svg>\n `;\n }, 2000);\n } catch (e) {\n console.error('Copy failed:', e);\n }\n });\n }\n }\n}\n\ndefine('mu-code', MuCode);\n",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.mu-spinner{display:inline-block}.mu-spinner__circle{display:block;border:3px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin var(--md-sys-motion-duration-extra-long1) linear infinite}.mu-spinner--xs .mu-spinner__circle{width:16px;height:16px;border-width:2px}.mu-spinner--sm .mu-spinner__circle{width:20px;height:20px;border-width:2px}.mu-spinner--md .mu-spinner__circle{width:
|
|
1
|
+
.mu-spinner{display:inline-block}.mu-spinner__circle{display:block;border:3px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin var(--md-sys-motion-duration-extra-long1) linear infinite}.mu-spinner--xs .mu-spinner__circle{width:16px;height:16px;border-width:2px}.mu-spinner--sm .mu-spinner__circle{width:20px;height:20px;border-width:2px}.mu-spinner--md .mu-spinner__circle{width:32px;height:32px;border-width:3px}.mu-spinner--lg .mu-spinner__circle{width:48px;height:48px;border-width:4px}.mu-spinner--xl .mu-spinner__circle{width:48px;height:48px;border-width:4px}.mu-spinner--primary{color:var(--md-sys-color-primary)}.mu-spinner--secondary{color:var(--md-sys-color-secondary)}.mu-spinner--current{color:currentColor}.mu-spinner--white{color:white}
|
package/docs/getting-started.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
### NPM / Bun
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install microui
|
|
8
|
+
npm install microui-wc
|
|
9
9
|
# or
|
|
10
|
-
bun add microui
|
|
10
|
+
bun add microui-wc
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
### CDN
|
|
@@ -20,7 +20,7 @@ bun add microui
|
|
|
20
20
|
### From Source
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
git clone https://github.com/
|
|
23
|
+
git clone https://github.com/GiuseppeScottoLavina/microUI.git
|
|
24
24
|
cd microui
|
|
25
25
|
bun install
|
|
26
26
|
bun run build
|