@k3000/ce 0.0.1 → 0.2.0

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.
@@ -0,0 +1,96 @@
1
+ import {bindNode, useEvent} from "../ce.mjs";
2
+
3
+ export default class RouterView extends HTMLElement {
4
+
5
+ routes = []
6
+
7
+ event = useEvent()
8
+
9
+ constructor() {
10
+
11
+ super()
12
+
13
+ this.event.add('route.to', (...args) => this.routeTo(...args))
14
+
15
+ this.event.add('init routers', routes => {
16
+
17
+ this.routes = routes
18
+
19
+ setTimeout(() => this.init(), 43)
20
+
21
+ }, {preValue: true, once: true})
22
+ }
23
+
24
+ routeTo(target, params) {
25
+
26
+ const route = this.routes.find(route => route.path === target)
27
+
28
+ if (!route) return new ErrorEvent(`route to "${target}" not found`)
29
+
30
+ location.hash = '#' + target
31
+
32
+ route.component.params = params
33
+ }
34
+
35
+ init() {
36
+
37
+ for (const route of this.routes) {
38
+
39
+ if(typeof route.component === 'string') {
40
+
41
+ route.component = document.createElement(route.component)
42
+
43
+ if (route.dir) {
44
+
45
+ route.component.setAttribute('dir', route.dir)
46
+ }
47
+ }
48
+ }
49
+
50
+ if (this.routes.some(route => route.path !== '/404')) {
51
+
52
+ const route = {
53
+ path: '/404',
54
+ dir: './pages',
55
+ component: document.createElement('not-found'),
56
+ }
57
+
58
+ route.component.setAttribute('dir', route.dir)
59
+
60
+ this.routes.push(route)
61
+ }
62
+
63
+ addEventListener('hashchange', () => this.hashListener())
64
+
65
+ this.hashListener()
66
+ }
67
+
68
+ hashListener() {
69
+
70
+ const currentHash = location.hash.slice(1) || '/';
71
+
72
+ this.event.dispatch('route.change', currentHash)
73
+
74
+ const route = this.routes.find(route => route.path === currentHash) || this.routes.find(route => route.path === '/404')
75
+
76
+ if (!route) return
77
+
78
+ const component = route.component.cloneNode(true)
79
+
80
+ if (currentHash === '/system/user') {
81
+
82
+ // debugger
83
+ }
84
+
85
+ bindNode(this, component, true)
86
+
87
+ if (this.firstElementChild) {
88
+
89
+ this.replaceChild(component, this.firstElementChild)
90
+
91
+ } else {
92
+
93
+ this.appendChild(component)
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,211 @@
1
+ const allUsers = Array.from({ length: 55 }, (_, i) => ({
2
+ id: 1000 + i,
3
+ username: `user_${i + 1}`,
4
+ name: `System User ${i + 1}`,
5
+ status: i % 3 === 0 ? 'inactive' : 'active',
6
+ password: '',
7
+ createdAt: new Date().toLocaleDateString()
8
+ }))
9
+ /**
10
+ * @param {Number} page
11
+ * @param {Number} size
12
+ * @param {{
13
+ * key: String
14
+ * }} query
15
+ * @returns {Promise<{
16
+ * list: {id, username: string, name: string, status: string, password: string, createdAt: string}[],
17
+ * total: number,
18
+ * totalPage: number,
19
+ * }>}
20
+ */
21
+ export const getUsersByPage = (page = 1, size = 10, query = {}) => {
22
+
23
+ let list = allUsers
24
+
25
+ if (query.key) {
26
+
27
+ const lower = query.key.toLowerCase()
28
+
29
+ list = list.filter(u =>
30
+ u.username.toLowerCase().includes(lower) ||
31
+ u.name.toLowerCase().includes(lower)
32
+ )
33
+ }
34
+
35
+ const start = (page - 1) * size
36
+ const end = start + size
37
+
38
+ return Promise.resolve({
39
+ list: list.slice(start, end),
40
+ total: list.length,
41
+ totalPage: Math.ceil(list.length / size) || 1,
42
+ })
43
+ }
44
+
45
+ export const deleteUser = (id) => {
46
+ const index = allUsers.findIndex(u => u.id === id)
47
+ if (index !== -1) {
48
+ allUsers.splice(index, 1)
49
+ return Promise.resolve(true)
50
+ }
51
+ return Promise.resolve(false)
52
+ }
53
+
54
+ export const saveUser = (user) => {
55
+ if (user.id) {
56
+ const index = allUsers.findIndex(u => u.id === user.id)
57
+ if (index !== -1) {
58
+ allUsers[index] = { ...allUsers[index], ...user }
59
+ return Promise.resolve(allUsers[index])
60
+ }
61
+ } else {
62
+ const newUser = {
63
+ ...user,
64
+ id: Date.now(),
65
+ createdAt: new Date().toLocaleDateString(),
66
+ status: 'active'
67
+ }
68
+ allUsers.unshift(newUser)
69
+ return Promise.resolve(newUser)
70
+ }
71
+ return Promise.reject('User not found')
72
+ }
73
+
74
+ const allRoles = Array.from({ length: 45 }, (_, i) => {
75
+ const rolesList = ['Admin', 'Editor', 'Viewer', 'Manager', 'Developer', 'Designer', 'QA', 'Support', 'Sales', 'HR']
76
+ return {
77
+ id: 2000 + i,
78
+ roleName: rolesList[i % rolesList.length] + ` ${Math.floor(i / rolesList.length) + 1}`,
79
+ description: `Permission set for ${rolesList[i % rolesList.length]}`,
80
+ status: i % 5 === 0 ? 'inactive' : 'active',
81
+ createdAt: new Date().toLocaleDateString()
82
+ }
83
+ })
84
+
85
+ export const getRolesByPage = (page = 1, size = 10, query = {}) => {
86
+ let list = allRoles
87
+ if (query.key) {
88
+ const lower = query.key.toLowerCase()
89
+ list = list.filter(r =>
90
+ r.roleName.toLowerCase().includes(lower) ||
91
+ r.description.toLowerCase().includes(lower)
92
+ )
93
+ }
94
+ const start = (page - 1) * size
95
+ const end = start + size
96
+ return Promise.resolve({
97
+ list: list.slice(start, end),
98
+ total: list.length,
99
+ totalPage: Math.ceil(list.length / size) || 1,
100
+ })
101
+ }
102
+
103
+ export const deleteRole = (id) => {
104
+ const index = allRoles.findIndex(r => r.id === id)
105
+ if (index !== -1) {
106
+ allRoles.splice(index, 1)
107
+ return Promise.resolve(true)
108
+ }
109
+ return Promise.resolve(false)
110
+ }
111
+
112
+ export const saveRole = (role) => {
113
+ if (role.id) {
114
+ const index = allRoles.findIndex(r => r.id === role.id)
115
+ if (index !== -1) {
116
+ allRoles[index] = { ...allRoles[index], ...role }
117
+ return Promise.resolve(allRoles[index])
118
+ }
119
+ } else {
120
+ const newRole = {
121
+ ...role,
122
+ id: Date.now(),
123
+ createdAt: new Date().toLocaleDateString(),
124
+ status: 'active'
125
+ }
126
+ allRoles.unshift(newRole)
127
+ return Promise.resolve(newRole)
128
+ }
129
+ return Promise.reject('Role not found')
130
+ }
131
+
132
+ const allMenus = [
133
+ { id: 3001, menuName: '控制台', path: '/dashboard', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6', sort: 1, status: 'active', createdAt: '2024-01-01' },
134
+ { id: 3002, menuName: '系统管理', path: '/system', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z', sort: 2, status: 'active', createdAt: '2024-01-02' },
135
+ { id: 3003, menuName: '用户管理', path: '/system/user', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z', sort: 1, status: 'active', createdAt: '2024-01-03' },
136
+ { id: 3004, menuName: '角色管理', path: '/system/role', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z', sort: 2, status: 'active', createdAt: '2024-01-04' }
137
+ ]
138
+
139
+ export const getMenusByPage = (page = 1, size = 10, query = {}) => {
140
+ let list = allMenus
141
+ if (query.key) {
142
+ const lower = query.key.toLowerCase()
143
+ list = list.filter(m =>
144
+ m.menuName.toLowerCase().includes(lower) ||
145
+ m.path.toLowerCase().includes(lower)
146
+ )
147
+ }
148
+ const start = (page - 1) * size
149
+ const end = start + size
150
+ return Promise.resolve({
151
+ list: list.slice(start, end),
152
+ total: list.length,
153
+ totalPage: Math.ceil(list.length / size) || 1,
154
+ })
155
+ }
156
+
157
+ export const deleteMenu = (id) => {
158
+ const index = allMenus.findIndex(m => m.id == id)
159
+ if (index !== -1) {
160
+ allMenus.splice(index, 1)
161
+ return Promise.resolve(true)
162
+ }
163
+ return Promise.resolve(false)
164
+ }
165
+
166
+ export const saveMenu = (menu) => {
167
+ if (menu.id) {
168
+ const index = allMenus.findIndex(m => m.id == menu.id)
169
+ if (index !== -1) {
170
+ allMenus[index] = { ...allMenus[index], ...menu }
171
+ return Promise.resolve(allMenus[index])
172
+ }
173
+ } else {
174
+ const newMenu = {
175
+ ...menu,
176
+ id: Date.now(),
177
+ createdAt: new Date().toLocaleDateString(),
178
+ status: 'active'
179
+ }
180
+ allMenus.unshift(newMenu)
181
+ return Promise.resolve(newMenu)
182
+ }
183
+ return Promise.reject('Menu not found')
184
+ }
185
+
186
+ export const toggleMenuStatus = (id) => {
187
+ const menu = allMenus.find(m => m.id == id)
188
+ if (menu) {
189
+ menu.status = menu.status === 'active' ? 'inactive' : 'active'
190
+ return Promise.resolve(menu)
191
+ }
192
+ return Promise.reject('Menu not found')
193
+ }
194
+
195
+ export const toggleRoleStatus = (id) => {
196
+ const role = allRoles.find(r => r.id == id)
197
+ if (role) {
198
+ role.status = role.status === 'active' ? 'inactive' : 'active'
199
+ return Promise.resolve(role)
200
+ }
201
+ return Promise.reject('Role not found')
202
+ }
203
+
204
+ export const toggleUserStatus = (id) => {
205
+ const user = allUsers.find(u => u.id == id)
206
+ if (user) {
207
+ user.status = user.status === 'active' ? 'inactive' : 'active'
208
+ return Promise.resolve(user)
209
+ }
210
+ return Promise.reject('User not found')
211
+ }
@@ -0,0 +1,63 @@
1
+ import {useAttr, useRef} from "../../ce.mjs";
2
+
3
+ export const innerHTML = `
4
+ <button data-action="{{action}}" data-id="{{id}}" class="p-2 text-gray-400 {{typeClass}} rounded-lg transition-colors" title="{{title}}">
5
+ <svg class="w-4 h-4 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
+ <path ref="path" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
7
+ </svg>
8
+ </button>
9
+ `
10
+
11
+ export default class extends HTMLElement {
12
+ action = ''
13
+ id = ''
14
+ title = ''
15
+ icon = ''
16
+ typeClass = ''
17
+
18
+ constructor() {
19
+ super()
20
+ this.style.display = 'contents'
21
+ const attr = useAttr(this)
22
+
23
+ const presets = {
24
+ toggle: {
25
+ icon: 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636',
26
+ type: 'orange',
27
+ title: '切换状态'
28
+ },
29
+ edit: {
30
+ icon: 'M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z',
31
+ type: 'blue',
32
+ title: '编辑'
33
+ },
34
+ delete: {
35
+ icon: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
36
+ type: 'red',
37
+ title: '删除'
38
+ }
39
+ }
40
+
41
+ const preset = presets[attr.action] || {}
42
+
43
+ this.action = attr.action || ''
44
+ this.id = attr.id || ''
45
+ this.title = attr.title || preset.title || ''
46
+ this.icon = attr.icon || preset.icon || ''
47
+
48
+ const type = attr.type || preset.type || 'blue'
49
+ const types = {
50
+ orange: 'hover:text-orange-500 hover:bg-orange-50 dark:hover:bg-orange-500/10',
51
+ blue: 'hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/10',
52
+ red: 'hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10'
53
+ }
54
+ this.typeClass = types[type] || types.blue
55
+ }
56
+
57
+ ready() {
58
+
59
+ const refs = useRef(this)
60
+
61
+ refs.path.setAttribute('d', this.icon)
62
+ }
63
+ }
@@ -0,0 +1,26 @@
1
+ import {useAttr} from "../../ce.mjs";
2
+
3
+ export const innerHTML = `
4
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
5
+ <div>
6
+ <h1 class="text-2xl font-bold text-gray-800 dark:text-white tracking-tight">{{title}}</h1>
7
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{description}}</p>
8
+ </div>
9
+ <div class="flex items-center gap-3">
10
+ <slot></slot>
11
+ </div>
12
+ </div>
13
+ `
14
+
15
+ export default class extends HTMLElement {
16
+ title = ''
17
+ description = ''
18
+
19
+ constructor() {
20
+ super()
21
+ const attr = useAttr(this)
22
+ this.title = attr.title
23
+ this.description = attr.description
24
+ // console.log([this])
25
+ }
26
+ }
@@ -0,0 +1,44 @@
1
+ import { useAttr, useWatch } from "../../ce.mjs";
2
+
3
+ export const innerHTML = `
4
+ <div class="space-y-1.5">
5
+ <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300">{{label}}</label>
6
+ <input connect="{{type !== 'textarea'}}" type="{{type}}" value="{{value}}" oninput="{{onInput}}" class="w-full px-4 py-2.5 rounded-xl bg-white/50 dark:bg-gray-900/50 border border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all text-sm backdrop-blur-sm" placeholder="{{placeholder}}" />
7
+ <textarea connect="{{type === 'textarea'}}" oninput="{{onInput}}" class="w-full px-4 py-2.5 rounded-xl bg-white/50 dark:bg-gray-900/50 border border-gray-200/50 dark:border-gray-700/50 focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all text-sm backdrop-blur-sm h-24 resize-none" placeholder="{{placeholder}}">{{value}}</textarea>
8
+ </div>
9
+ `
10
+
11
+ export default class extends HTMLElement {
12
+ label = ''
13
+ type = 'text'
14
+ prop = ''
15
+ value = ''
16
+ placeholder = ''
17
+
18
+ constructor() {
19
+ super()
20
+ this.style.display = 'block'
21
+ const attr = useAttr(this)
22
+ this.label = attr.label || ''
23
+ this.type = attr.type || 'text'
24
+ this.prop = attr.prop || ''
25
+ this.placeholder = attr.placeholder || ''
26
+ }
27
+
28
+ ready() {
29
+ const watch = useWatch(this.obj)
30
+ watch({
31
+ [this.prop]: value => {
32
+ this.value = value
33
+ }
34
+ })
35
+ }
36
+
37
+ onInput(e) {
38
+ this.obj[this.prop] = e.target.value
39
+ // If there's an external oninput/onchange, it will be handled by the framework's bind logic
40
+ // But we want to ensure any custom event is dispatched if needed.
41
+ // In this framework, native events on the inner input are usually mapped via {{onInput}}
42
+ // to a method on the parent scope.
43
+ }
44
+ }
@@ -0,0 +1,45 @@
1
+ import { useAttr } from "../../ce.mjs";
2
+
3
+ export const innerHTML = `
4
+ <style>
5
+ .modal-enter { opacity: 0; transform: scale(0.95); }
6
+ .modal-create { opacity: 1; transform: scale(1); }
7
+ </style>
8
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity duration-300 {{visible ? '' : 'hidden'}}" onclick="{{onClose}}">
9
+ <!-- Modal Content -->
10
+ <div class="bg-white/80 dark:bg-gray-800/90 backdrop-blur-2xl rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all scale-100 border border-white/40 dark:border-white/10" onclick="event.stopPropagation()">
11
+ <div class="px-6 py-4 border-b border-gray-100/50 dark:border-gray-700/50 flex items-center justify-between bg-white/30 dark:bg-white/5">
12
+ <h3 class="text-lg font-bold text-gray-900 dark:text-white">{{title}}</h3>
13
+ <button onclick="{{onClose}}" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
14
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
15
+ </button>
16
+ </div>
17
+
18
+ <div class="p-6 space-y-5">
19
+ <slot></slot>
20
+ </div>
21
+
22
+ <div class="px-6 py-4 bg-gray-50/30 dark:bg-gray-800/30 border-t border-gray-100/50 dark:border-gray-700/50 flex justify-end gap-3">
23
+ <button onclick="{{onClose}}" class="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 transition-colors bg-white/50 dark:bg-gray-700/50 border border-gray-200/50 dark:border-gray-600/50 rounded-xl hover:shadow-sm hover:bg-white/80 dark:hover:bg-gray-700/80">取消</button>
24
+ <button onclick="{{onSave}}" class="px-5 py-2 text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 rounded-xl shadow-lg shadow-blue-500/30 transition-all hover:-translate-y-0.5 active:translate-y-0 active:scale-95">保存</button>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ `
29
+
30
+ export default class extends HTMLElement {
31
+ visible = false
32
+ title = ''
33
+
34
+ constructor() {
35
+ super()
36
+ }
37
+
38
+ onClose() {
39
+ if (this.onclose) this.onclose()
40
+ }
41
+
42
+ onSave() {
43
+ if (this.onsave) this.onsave()
44
+ }
45
+ }
@@ -0,0 +1,87 @@
1
+ import {throttle, useWatch} from "../../ce.mjs";
2
+
3
+ export const innerHTML = `
4
+ <div class="px-6 py-4 border-t border-white/20 dark:border-white/5 bg-white/30 dark:bg-white/5 backdrop-blur-md flex items-center justify-between">
5
+ <div class="text-sm text-gray-500 dark:text-gray-400">
6
+ {{total}} 显示 <span class="font-medium text-gray-900 dark:text-gray-100">{{(currentPage - 1) * pageSize + 1}}</span> 到 <span class="font-medium text-gray-900 dark:text-gray-100">{{Math.min(currentPage * pageSize, total)}}</span> 条,共 <span class="font-medium text-gray-900 dark:text-gray-100">{{total}}</span> 条
7
+ </div>
8
+
9
+ <div class="flex items-center gap-2">
10
+ <button onclick="{{prevPage}}" class="p-2 rounded-lg hover:bg-white/50 dark:hover:bg-white/10 text-gray-500 dark:text-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all" disabled="{{currentPage === 1}}">
11
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
12
+ </button>
13
+
14
+ <div class="flex items-center gap-1">
15
+ <button each="{{pages}}" onclick="{{_.changePage}}" data-page="{{index}}" class="w-8 h-8 rounded-lg text-sm font-medium transition-all {{index === _.currentPage ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-white/10'}}">
16
+ {{index}}
17
+ </button>
18
+ </div>
19
+
20
+ <button onclick="{{nextPage}}" class="p-2 rounded-lg hover:bg-white/50 dark:hover:bg-white/10 text-gray-500 dark:text-gray-400 disabled:opacity-50 disabled:cursor-not-allowed transition-all" disabled="{{currentPage === totalPage}}">
21
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
22
+ </button>
23
+ </div>
24
+ </div>
25
+ `;
26
+
27
+ export default class extends HTMLElement {
28
+ totalPage = 0
29
+ pages = []
30
+
31
+ constructor() {
32
+ super()
33
+
34
+ const watch = useWatch(this)
35
+
36
+ watch({
37
+ total: (total) => {
38
+ this.generatePages(total)
39
+ }
40
+ })
41
+
42
+ this.generatePages()
43
+ }
44
+
45
+ generatePages(total = this.total) {
46
+ this.totalPage = Math.ceil(total / this.pageSize) || 1
47
+ const maxVisible = 5
48
+ let startPage = Math.max(1, this.currentPage - Math.floor(maxVisible / 2))
49
+ let endPage = Math.min(this.totalPage, startPage + maxVisible - 1)
50
+
51
+ if (endPage - startPage + 1 < maxVisible) {
52
+ startPage = Math.max(1, endPage - maxVisible + 1)
53
+ }
54
+
55
+ const list = []
56
+ for (let i = startPage; i <= endPage; i++) {
57
+ list.push({index: i})
58
+ }
59
+ this.pages = list
60
+ }
61
+
62
+ prevPage() {
63
+ if (this.currentPage > 1) {
64
+ this.currentPage--
65
+ this.emitChange()
66
+ }
67
+ }
68
+
69
+ nextPage() {
70
+ if (this.currentPage < this.totalPage) {
71
+ this.currentPage++
72
+ this.emitChange()
73
+ }
74
+ }
75
+
76
+ changePage(e) {
77
+ const btn = e.target.closest('button')
78
+ if (btn && btn.dataset.page) {
79
+ this.currentPage = parseInt(btn.dataset.page)
80
+ this.emitChange()
81
+ }
82
+ }
83
+
84
+ emitChange() {
85
+ this.onchange(this.currentPage)
86
+ }
87
+ }
@@ -0,0 +1,39 @@
1
+ import { useAttr } from "../../ce.mjs";
2
+
3
+ export const outerHTML = `
4
+ <div class="relative group">
5
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
6
+ <svg class="h-4 w-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
7
+ </div>
8
+ <input type="text" oninput="{{onSearch}}" placeholder="{{placeholder}}" class="pl-10 pr-4 py-2 bg-white/50 dark:bg-gray-800/50 border border-white/40 dark:border-white/10 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all w-64 shadow-sm placeholder-gray-400 dark:text-gray-200 backdrop-blur-md" />
9
+ </div>
10
+ <button onclick="{{onadd}}" class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-xl shadow-lg shadow-blue-500/30 transition-all hover:scale-105 active:scale-95">
11
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
12
+ {{buttonText}}
13
+ </button>
14
+ `
15
+
16
+ export default class extends HTMLElement {
17
+ placeholder = 'Search...'
18
+ buttonText = 'Add'
19
+
20
+ constructor() {
21
+ super()
22
+ this.style.display = 'contents'
23
+ const attr = useAttr(this)
24
+ if (attr.placeholder) this.placeholder = attr.placeholder
25
+ if (attr['button-text']) this.buttonText = attr['button-text']
26
+ }
27
+
28
+ onSearch(e) {
29
+ const event = new CustomEvent('search', { detail: { value: e.target.value } })
30
+ this.dispatchEvent(event)
31
+ if (this.onsearch) this.onsearch(event)
32
+ }
33
+
34
+ /*onAdd() {
35
+ const event = new CustomEvent('add')
36
+ this.dispatchEvent(event)
37
+ if (this.onadd) this.onadd(event)
38
+ }*/
39
+ }
@@ -0,0 +1,46 @@
1
+
2
+ export const shadowRoot = `
3
+ <style>
4
+ menu {
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+ .layout-container {
9
+ display: grid;
10
+ height: 100vh;
11
+ grid-template-columns: auto 1fr;
12
+ grid-template-rows: auto 1fr;
13
+
14
+ > header {
15
+ grid-column: span 2;
16
+ }
17
+
18
+ > main {
19
+ padding: 12px;
20
+ }
21
+
22
+ > menu, > main {
23
+ overflow-x: hidden;
24
+ overflow-y: auto;
25
+ }
26
+ }
27
+ </style>
28
+ <div class="layout-container">
29
+ <header>
30
+ <slot name="header"></slot>
31
+ </header>
32
+ <menu>
33
+ <slot name="menu"></slot>
34
+ </menu>
35
+ <main>
36
+ <slot name="main"></slot>
37
+ </main>
38
+ </div>
39
+ `
40
+
41
+ export default class extends HTMLElement {
42
+ constructor() {
43
+
44
+ super()
45
+ }
46
+ }