diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf3f731 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +test-results/ +playwright-report/ +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f3b1a9f --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} diff --git a/CHANGELOG.md b/CHANGELOG.md index 467fcce..0a02e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# @web/kanban 更新日志 +# @apigo.cc/kanban 更新日志 + +## v1.0.2 (2026-06-12) +- **架构对齐**: 遵循 `base` 包新设计,彻底消灭用户侧 ESM 强依赖。 +- **组件注册**: 支持 `` 全局组件声明式用法(兼容 `$state` 框架)。 +- **构建优化**: 产物调整为纯 UMD 模式,通过 `globalThis.Kanban` 暴露 API。 ## v1.0.0 (2026-05-29) - **Feat**: 初始化看板引擎,支持多列渲染。 diff --git a/README.md b/README.md index b1c81ca..9b66d17 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,53 @@ -# @web/kanban +# @apigo.cc/kanban API 手册 -原生 ESM 驱动的极简看板引擎,专注于无状态渲染与高性能拖拽。 +原生 Web Component 编写的轻量级看板组件。支持响应式数据绑定与自动拖拽排序。 -## 核心特性 -- **零依赖**: 仅使用原生 Web Components 与 Drag & Drop API。 -- **双轨分发**: 提供源码版 `kanban.js` 与压缩版 `kanban.min.js`。 -- **高性能**: 支持数百个看板项的丝滑交互。 +--- -## 快速上手 +## 1. 引入方式 (UMD 优先) -### 1. 引入模块 -使用 `loader.js` 或直接通过 `importmap` 引入: +在 HTML 中引入脚本即刻注册 `` 自定义标签。 ```html - - + + ``` -### 2. 使用组件 +--- + +## 2. 核心用法 + +### 组件化用法 (推荐) +直接在 HTML 中使用 `` 标签,通过 `state` 进行数据绑定。 + ```html - - - +
+ +
``` -## API 参考 +### JS 直接调用 +... +--- -### 属性 (Properties) -- `data`: `Array` - 获取或设置看板数据。 +## 3. API 参考 -### 数据结构 (Types) -```typescript -interface Column { - id: string; - title: string; - items: Item[]; -} +### 属性 +- **`.data`**: 设置或获取看板数组。 -interface Item { - id: string; - content: string; -} -``` +### 事件 +- **`@update`**: 拖拽结束且位置变动时触发,`detail` 为全量新数据。 -### 事件 (Events) -- `@update`: 当看板项位置发生变化时触发。`event.detail` 包含完整的 `Column[]` 数据。 +--- + +## 开发者提示 (AI 必读) +1. **样式隔离**: 组件使用 Shadow DOM,外部 CSS 无法影响内部。 +2. **全局变量**: UMD 模式下,`Kanban` 类自动挂载到 `window`。 diff --git a/TEST.md b/TEST.md index 1183438..0b43ce9 100644 --- a/TEST.md +++ b/TEST.md @@ -1,4 +1,4 @@ -# @web/kanban 测试报告 +# @apigo.cc/kanban 测试报告 ## 基准性能 (Benchmark) - **环境**: Desktop Chrome (Playwright) diff --git a/dist/kanban.js b/dist/kanban.js index 30555a3..2d1acb3 100644 --- a/dist/kanban.js +++ b/dist/kanban.js @@ -1,4 +1,7 @@ -var __typeError = (msg) => { +(function(global, factory) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoKanban = {})); +})(this, function(exports2) { + "use strict";var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); @@ -6,29 +9,30 @@ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read fr var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); -var _data, _Kanban_instances, render_fn, initDragAndDrop_fn, moveItem_fn; -class Kanban extends HTMLElement { - constructor() { - super(); - __privateAdd(this, _Kanban_instances); - __privateAdd(this, _data, []); - this.attachShadow({ mode: "open" }); + + var _data, _Kanban_instances, render_fn, initDragAndDrop_fn, moveItem_fn; + class Kanban extends HTMLElement { + constructor() { + super(); + __privateAdd(this, _Kanban_instances); + __privateAdd(this, _data, []); + this.attachShadow({ mode: "open" }); + } + connectedCallback() { + __privateMethod(this, _Kanban_instances, render_fn).call(this); + } + get data() { + return __privateGet(this, _data); + } + set data(value) { + __privateSet(this, _data, value); + __privateMethod(this, _Kanban_instances, render_fn).call(this); + } } - connectedCallback() { - __privateMethod(this, _Kanban_instances, render_fn).call(this); - } - get data() { - return __privateGet(this, _data); - } - set data(value) { - __privateSet(this, _data, value); - __privateMethod(this, _Kanban_instances, render_fn).call(this); - } -} -_data = new WeakMap(); -_Kanban_instances = new WeakSet(); -render_fn = function() { - const style = ` + _data = new WeakMap(); + _Kanban_instances = new WeakSet(); + render_fn = function() { + const style = ` :host { display: flex; gap: 16px; @@ -75,7 +79,7 @@ render_fn = function() { background: rgba(0,0,0,0.05); } `; - this.shadowRoot.innerHTML = ` + this.shadowRoot.innerHTML = ` ${__privateGet(this, _data).map((col) => `
@@ -92,54 +96,70 @@ render_fn = function() {
`).join("")} `; - __privateMethod(this, _Kanban_instances, initDragAndDrop_fn).call(this); -}; -initDragAndDrop_fn = function() { - const items = this.shadowRoot.querySelectorAll(".item"); - const containers = this.shadowRoot.querySelectorAll(".items-container"); - items.forEach((item) => { - item.addEventListener("dragstart", (e) => { - item.classList.add("dragging"); - e.dataTransfer.setData("text/plain", JSON.stringify({ - itemId: item.dataset.id, - fromColumnId: item.dataset.columnId - })); + __privateMethod(this, _Kanban_instances, initDragAndDrop_fn).call(this); + }; + initDragAndDrop_fn = function() { + const items = this.shadowRoot.querySelectorAll(".item"); + const containers = this.shadowRoot.querySelectorAll(".items-container"); + items.forEach((item) => { + item.addEventListener("dragstart", (e) => { + item.classList.add("dragging"); + e.dataTransfer.setData("text/plain", JSON.stringify({ + itemId: item.dataset.id, + fromColumnId: item.dataset.columnId + })); + }); + item.addEventListener("dragend", () => { + item.classList.remove("dragging"); + }); }); - item.addEventListener("dragend", () => { - item.classList.remove("dragging"); + containers.forEach((container) => { + container.addEventListener("dragover", (e) => { + e.preventDefault(); + container.classList.add("drag-over"); + }); + container.addEventListener("dragleave", () => { + container.classList.remove("drag-over"); + }); + container.addEventListener("drop", (e) => { + e.preventDefault(); + container.classList.remove("drag-over"); + const dragData = JSON.parse(e.dataTransfer.getData("text/plain")); + __privateMethod(this, _Kanban_instances, moveItem_fn).call(this, dragData.itemId, dragData.fromColumnId, container.dataset.columnId); + }); }); - }); - containers.forEach((container) => { - container.addEventListener("dragover", (e) => { - e.preventDefault(); - container.classList.add("drag-over"); + }; + moveItem_fn = function(itemId, fromColumnId, toColumnId) { + if (fromColumnId === toColumnId) return; + const fromCol = __privateGet(this, _data).find((c) => c.id === fromColumnId); + const toCol = __privateGet(this, _data).find((c) => c.id === toColumnId); + const itemIndex = fromCol.items.findIndex((i) => i.id === itemId); + const [item] = fromCol.items.splice(itemIndex, 1); + toCol.items.push(item); + __privateMethod(this, _Kanban_instances, render_fn).call(this); + this.dispatchEvent(new CustomEvent("@update", { + detail: __privateGet(this, _data), + bubbles: true, + composed: true + })); + }; + customElements.define("web-kanban", Kanban); + if (typeof globalThis !== "undefined" && globalThis.Component) { + globalThis.Component.register("Kanban", (container) => { + const kanban = document.createElement("web-kanban"); + container.appendChild(kanban); + container.kanbanInstance = kanban; + container.state.__watch("data", (val) => kanban.data = val); + if (container.state.data) kanban.data = container.state.data; + kanban.addEventListener("@update", (e) => { + container.state.data = e.detail; + container.dispatchEvent(new CustomEvent("update", { detail: e.detail })); + }); }); - container.addEventListener("dragleave", () => { - container.classList.remove("drag-over"); - }); - container.addEventListener("drop", (e) => { - e.preventDefault(); - container.classList.remove("drag-over"); - const dragData = JSON.parse(e.dataTransfer.getData("text/plain")); - __privateMethod(this, _Kanban_instances, moveItem_fn).call(this, dragData.itemId, dragData.fromColumnId, container.dataset.columnId); - }); - }); -}; -moveItem_fn = function(itemId, fromColumnId, toColumnId) { - if (fromColumnId === toColumnId) return; - const fromCol = __privateGet(this, _data).find((c) => c.id === fromColumnId); - const toCol = __privateGet(this, _data).find((c) => c.id === toColumnId); - const itemIndex = fromCol.items.findIndex((i) => i.id === itemId); - const [item] = fromCol.items.splice(itemIndex, 1); - toCol.items.push(item); - __privateMethod(this, _Kanban_instances, render_fn).call(this); - this.dispatchEvent(new CustomEvent("@update", { - detail: __privateGet(this, _data), - bubbles: true, - composed: true - })); -}; -customElements.define("web-kanban", Kanban); -export { - Kanban -}; + } + if (typeof globalThis !== "undefined") { + globalThis.Kanban = Kanban; + } + exports2.Kanban = Kanban; + Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); +}); diff --git a/dist/kanban.min.js b/dist/kanban.min.js index 330b92f..77046e0 100644 --- a/dist/kanban.min.js +++ b/dist/kanban.min.js @@ -1 +1 @@ -var n,t,e,a,i,s=n=>{throw TypeError(n)},d=(n,t,e)=>t.has(n)||s("Cannot "+e),r=(n,t,e)=>(d(n,t,"read from private field"),e?e.call(n):t.get(n)),o=(n,t,e)=>t.has(n)?s("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(n):t.set(n,e),l=(n,t,e)=>(d(n,t,"access private method"),e);class c extends HTMLElement{constructor(){super(),o(this,t),o(this,n,[]),this.attachShadow({mode:"open"})}connectedCallback(){l(this,t,e).call(this)}get data(){return r(this,n)}set data(a){((n,t,e,a)=>{d(n,t,"write to private field"),a?a.call(n,e):t.set(n,e)})(this,n,a),l(this,t,e).call(this)}}n=new WeakMap,t=new WeakSet,e=function(){this.shadowRoot.innerHTML=`\n \n ${r(this,n).map(n=>`\n
\n
\n ${n.title}\n
\n
\n ${(n.items||[]).map(t=>`\n
\n ${t.content}\n
\n `).join("")}\n
\n
\n `).join("")}\n `,l(this,t,a).call(this)},a=function(){const n=this.shadowRoot.querySelectorAll(".item"),e=this.shadowRoot.querySelectorAll(".items-container");n.forEach(n=>{n.addEventListener("dragstart",t=>{n.classList.add("dragging"),t.dataTransfer.setData("text/plain",JSON.stringify({itemId:n.dataset.id,fromColumnId:n.dataset.columnId}))}),n.addEventListener("dragend",()=>{n.classList.remove("dragging")})}),e.forEach(n=>{n.addEventListener("dragover",t=>{t.preventDefault(),n.classList.add("drag-over")}),n.addEventListener("dragleave",()=>{n.classList.remove("drag-over")}),n.addEventListener("drop",e=>{e.preventDefault(),n.classList.remove("drag-over");const a=JSON.parse(e.dataTransfer.getData("text/plain"));l(this,t,i).call(this,a.itemId,a.fromColumnId,n.dataset.columnId)})})},i=function(a,i,s){if(i===s)return;const d=r(this,n).find(n=>n.id===i),o=r(this,n).find(n=>n.id===s),c=d.items.findIndex(n=>n.id===a),[m]=d.items.splice(c,1);o.items.push(m),l(this,t,e).call(this),this.dispatchEvent(new CustomEvent("@update",{detail:r(this,n),bubbles:!0,composed:!0}))},customElements.define("web-kanban",c);export{c as Kanban}; +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ApigoKanban={})}(this,function(e){"use strict";var t,n,a,i,d,s=e=>{throw TypeError(e)},o=(e,t,n)=>t.has(e)||s("Cannot "+n),r=(e,t,n)=>(o(e,t,"read from private field"),n?n.call(e):t.get(e)),l=(e,t,n)=>t.has(e)?s("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),c=(e,t,n)=>(o(e,t,"access private method"),n);class p extends HTMLElement{constructor(){super(),l(this,n),l(this,t,[]),this.attachShadow({mode:"open"})}connectedCallback(){c(this,n,a).call(this)}get data(){return r(this,t)}set data(e){((e,t,n,a)=>{o(e,t,"write to private field"),a?a.call(e,n):t.set(e,n)})(this,t,e),c(this,n,a).call(this)}}t=new WeakMap,n=new WeakSet,a=function(){this.shadowRoot.innerHTML=`\n \n ${r(this,t).map(e=>`\n
\n
\n ${e.title}\n
\n
\n ${(e.items||[]).map(t=>`\n
\n ${t.content}\n
\n `).join("")}\n
\n
\n `).join("")}\n `,c(this,n,i).call(this)},i=function(){const e=this.shadowRoot.querySelectorAll(".item"),t=this.shadowRoot.querySelectorAll(".items-container");e.forEach(e=>{e.addEventListener("dragstart",t=>{e.classList.add("dragging"),t.dataTransfer.setData("text/plain",JSON.stringify({itemId:e.dataset.id,fromColumnId:e.dataset.columnId}))}),e.addEventListener("dragend",()=>{e.classList.remove("dragging")})}),t.forEach(e=>{e.addEventListener("dragover",t=>{t.preventDefault(),e.classList.add("drag-over")}),e.addEventListener("dragleave",()=>{e.classList.remove("drag-over")}),e.addEventListener("drop",t=>{t.preventDefault(),e.classList.remove("drag-over");const a=JSON.parse(t.dataTransfer.getData("text/plain"));c(this,n,d).call(this,a.itemId,a.fromColumnId,e.dataset.columnId)})})},d=function(e,i,d){if(i===d)return;const s=r(this,t).find(e=>e.id===i),o=r(this,t).find(e=>e.id===d),l=s.items.findIndex(t=>t.id===e),[p]=s.items.splice(l,1);o.items.push(p),c(this,n,a).call(this),this.dispatchEvent(new CustomEvent("@update",{detail:r(this,t),bubbles:!0,composed:!0}))},customElements.define("web-kanban",p),"undefined"!=typeof globalThis&&globalThis.Component&&globalThis.Component.register("Kanban",e=>{const t=document.createElement("web-kanban");e.appendChild(t),e.kanbanInstance=t,e.state.__watch("data",e=>t.data=e),e.state.data&&(t.data=e.state.data),t.addEventListener("@update",t=>{e.state.data=t.detail,e.dispatchEvent(new CustomEvent("update",{detail:t.detail}))})}),"undefined"!=typeof globalThis&&(globalThis.Kanban=p),e.Kanban=p,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); diff --git a/kanbanTODO.md b/kanbanTODO.md new file mode 100644 index 0000000..db151df --- /dev/null +++ b/kanbanTODO.md @@ -0,0 +1,15 @@ +# kanban TODO + +- [x] 架构对齐:Align with `base` project's "No ESM" design. +- [ ] 构建配置:Modify `vite.config.js` to output only UMD (`kanban.js` and `kanban.min.js`). +- [ ] 源码重构: + - [ ] 注册 `Kanban` 为全局组件 (`globalThis.Component.register`) 以对齐状态框架。 + - [ ] 确保 `Kanban` 类在 `globalThis` 上可用。 +- [ ] 测试验证:运行 `npm test` 确保功能无损。 +- [ ] 文档更新: + - [ ] 更新 `package.json` 版本号。 + - [ ] 更新 `README.md` 以反映组件化用法。 + - [ ] 更新 `CHANGELOG.md`。 + +## 当前状态 (v1.0.1) +- 首次渲染耗时: 9.40ms (from TEST.md) diff --git a/package-lock.json b/package-lock.json index 30b2e68..cb70ea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@web/kanban", + "name": "@apigo.cc/kanban", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@web/kanban", + "name": "@apigo.cc/kanban", "version": "1.0.0", "devDependencies": { "@playwright/test": "^1.40.0", diff --git a/package.json b/package.json index 2da0b5f..51810c4 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "@apigo.cc/kanban", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "main": "dist/kanban.js", - "module": "dist/kanban.js", "files": [ "dist" ], diff --git a/playwright-report/index.html b/playwright-report/index.html index 33dfb0c..351168c 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -87,4 +87,4 @@ Error generating stack: `+l.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/scripts/publish.js b/scripts/publish.js new file mode 100644 index 0000000..b562aa9 --- /dev/null +++ b/scripts/publish.js @@ -0,0 +1,48 @@ +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +try { + // 1. 获取最新 tag + let tag; + try { + tag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim(); + } catch (err) { + throw new Error('Failed to find git tags. Please make sure the repository has tags (e.g., v1.0.0) before publishing.'); + } + // 去掉 v 前缀 + const version = tag.startsWith('v') ? tag.slice(1) : tag; + + console.log(`Latest git tag: ${tag}, Version to publish: ${version}`); + + // 2. 读取并更新 package.json + const pkgPath = path.join(__dirname, '../package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + + // 保持原有名称(如果已经带有 @apigo.cc/ 前缀)或替换前缀 + if (!pkg.name.startsWith('@apigo.cc/')) { + const baseName = pkg.name.includes('/') ? pkg.name.split('/')[1] : pkg.name; + pkg.name = `@apigo.cc/${baseName}`; + } + pkg.version = version; + + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); + console.log(`Updated package.json: name=${pkg.name}, version=${pkg.version}`); + + // 3. 构建 + console.log('Running build...'); + execSync('npm run build', { stdio: 'inherit', cwd: path.join(__dirname, '..') }); + + // 4. 发布 + console.log('Publishing to npm...'); + const args = process.argv.slice(2).join(' '); + execSync(`npm publish --access public ${args}`, { stdio: 'inherit', cwd: path.join(__dirname, '..') }); + + console.log('Publish successful!'); +} catch (error) { + console.error('Publish failed:', error.message); + process.exit(1); +} diff --git a/src/index.js b/src/index.js index 092c9b6..db1ccd2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ /** - * @web/kanban + * @apigo.cc/kanban * 原生 ESM 看板引擎 */ @@ -152,3 +152,25 @@ export class Kanban extends HTMLElement { } customElements.define('web-kanban', Kanban); + +// 注册为全局组件以对齐状态框架 +if (typeof globalThis !== 'undefined' && globalThis.Component) { + globalThis.Component.register('Kanban', container => { + const kanban = document.createElement('web-kanban'); + container.appendChild(kanban); + container.kanbanInstance = kanban; + + container.state.__watch('data', val => kanban.data = val); + if (container.state.data) kanban.data = container.state.data; + + kanban.addEventListener('@update', e => { + container.state.data = e.detail; + container.dispatchEvent(new CustomEvent('update', { detail: e.detail })); + }); + }); +} + +// 挂载到全局 +if (typeof globalThis !== 'undefined') { + globalThis.Kanban = Kanban; +} diff --git a/test/index.html b/test/index.html index b08ed1f..a946ba3 100644 --- a/test/index.html +++ b/test/index.html @@ -10,7 +10,7 @@ @@ -20,7 +20,7 @@