ezfw-core 1.0.21 → 1.0.23

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.
Files changed (43) hide show
  1. package/components/EzBaseComponent.ts +100 -5
  2. package/components/EzComponent.ts +3 -3
  3. package/components/EzLabel.ts +12 -3
  4. package/components/avatar/EzAvatar.ts +84 -54
  5. package/components/badge/EzBadge.ts +43 -24
  6. package/components/button/EzButton.ts +5 -3
  7. package/components/button/EzButtonGroup.ts +7 -10
  8. package/components/card/EzCard.ts +2 -1
  9. package/components/chart/EzChart.ts +20 -15
  10. package/components/checkbox/EzCheckbox.ts +47 -43
  11. package/components/dataview/EzDataView.ts +14 -29
  12. package/components/dataview/modes/EzDataViewCards.ts +51 -41
  13. package/components/dataview/modes/EzDataViewGrid.ts +5 -2
  14. package/components/datepicker/EzDatePicker.ts +2 -2
  15. package/components/dialog/EzDialog.ts +84 -67
  16. package/components/dropdown/EzDropdown.ts +72 -58
  17. package/components/form/EzForm.ts +45 -37
  18. package/components/kanban/EzKanban.module.scss +221 -0
  19. package/components/kanban/EzKanban.ts +222 -0
  20. package/components/kanban/EzKanbanTypes.ts +166 -0
  21. package/components/kanban/board/EzKanbanBoard.ts +117 -0
  22. package/components/kanban/card/EzKanbanCard.module.scss +173 -0
  23. package/components/kanban/card/EzKanbanCard.ts +275 -0
  24. package/components/kanban/card/EzKanbanCardEditor.ts +209 -0
  25. package/components/kanban/column/EzKanbanColumn.ts +253 -0
  26. package/components/kanban/state/EzKanbanController.ts +373 -0
  27. package/components/kanban/state/EzKanbanDragDrop.ts +226 -0
  28. package/components/panel/EzPanel.ts +59 -68
  29. package/components/picker/EzPicker.module.scss +14 -0
  30. package/components/picker/EzPicker.ts +118 -0
  31. package/components/radio/EzRadio.ts +55 -47
  32. package/components/select/EzSelect.ts +48 -44
  33. package/components/skeleton/EzSkeleton.ts +31 -26
  34. package/components/switch/EzSwitch.ts +52 -44
  35. package/components/tabs/EzTabPanel.ts +52 -48
  36. package/components/textarea/EzTextarea.ts +69 -54
  37. package/components/timepicker/EzTimePicker.ts +2 -2
  38. package/components/tooltip/EzTooltip.ts +20 -33
  39. package/core/ez.ts +7 -0
  40. package/core/loader.ts +2 -0
  41. package/core/renderer.ts +80 -4
  42. package/core/styleShortcuts.ts +418 -0
  43. package/package.json +1 -1
@@ -0,0 +1,275 @@
1
+ import styles from './EzKanbanCard.module.scss';
2
+ import { cx } from '../../../utils/cssModules.js';
3
+ import { EzBaseComponent } from '../../EzBaseComponent.js';
4
+ import type { EzKanbanController } from '../state/EzKanbanController.js';
5
+ import type { EzKanbanDragDrop } from '../state/EzKanbanDragDrop.js';
6
+ import type { EzKanbanConfig, NormalizedColumn, KanbanCard } from '../EzKanbanTypes.js';
7
+
8
+ const cls = cx(styles);
9
+
10
+ declare const ez: {
11
+ _createElement(config: unknown): Promise<HTMLElement>;
12
+ define(name: string, cls: unknown): void;
13
+ dialog: {
14
+ open(opts: unknown): Promise<unknown>;
15
+ };
16
+ };
17
+
18
+ interface EzKanbanCardConfig {
19
+ card: KanbanCard;
20
+ column: NormalizedColumn;
21
+ kanbanController: EzKanbanController;
22
+ kanbanDragDrop: EzKanbanDragDrop;
23
+ kanbanConfig: EzKanbanConfig;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ export class EzKanbanCard extends EzBaseComponent {
28
+ declare config: EzKanbanCardConfig;
29
+
30
+ async render(): Promise<HTMLElement> {
31
+ const { card, column, kanbanController, kanbanDragDrop, kanbanConfig } = this.config;
32
+
33
+ if (kanbanConfig.cardRender) {
34
+ const customConfig = kanbanConfig.cardRender(card, column);
35
+ return await ez._createElement(customConfig);
36
+ }
37
+
38
+ const items: unknown[] = await this._buildContentItems();
39
+
40
+ const el = await ez._createElement({
41
+ eztype: 'div',
42
+ cls: this._getCardClasses(),
43
+ 'data-card-id': String(card.id),
44
+ draggable: kanbanConfig.cardsReorderable !== false,
45
+ items,
46
+ onClick: () => kanbanConfig.onCardClick?.(card),
47
+ onDblClick: () => {
48
+ if (kanbanConfig.onCardDoubleClick) {
49
+ kanbanConfig.onCardDoubleClick(card);
50
+ } else if (kanbanConfig.cardsEditable !== false) {
51
+ this._openEditDialog();
52
+ }
53
+ }
54
+ }) as HTMLElement;
55
+
56
+ if (kanbanConfig.cardsReorderable !== false) {
57
+ el.addEventListener('dragstart', (e) => {
58
+ kanbanDragDrop.handleCardDragStart(e, card, column._id);
59
+ });
60
+
61
+ el.addEventListener('dragend', () => {
62
+ kanbanDragDrop.handleCardDragEnd();
63
+ });
64
+ }
65
+
66
+ return el;
67
+ }
68
+
69
+ private _getCardClasses(): string {
70
+ const { card } = this.config;
71
+ const classes = ['ez-kanban-card', cls('card')];
72
+
73
+ if (card.priority) {
74
+ classes.push(cls(card.priority));
75
+ classes.push(`priority-${card.priority}`);
76
+ }
77
+
78
+ return classes.filter(Boolean).join(' ');
79
+ }
80
+
81
+ private async _buildContentItems(): Promise<unknown[]> {
82
+ const { card, kanbanConfig } = this.config;
83
+ const items: unknown[] = [];
84
+
85
+ // Cover image
86
+ if (kanbanConfig.showCoverImage && card.coverImage) {
87
+ items.push({
88
+ eztype: 'img',
89
+ cls: cls('cover'),
90
+ src: card.coverImage,
91
+ alt: ''
92
+ });
93
+ }
94
+
95
+ // Labels
96
+ if (kanbanConfig.showLabels !== false && card.labels?.length) {
97
+ items.push({
98
+ eztype: 'div',
99
+ cls: cls('labels'),
100
+ items: card.labels.map(label => ({
101
+ eztype: 'span',
102
+ cls: cls('label'),
103
+ style: { backgroundColor: label.color },
104
+ text: label.text
105
+ }))
106
+ });
107
+ }
108
+
109
+ // Title
110
+ items.push({
111
+ eztype: 'div',
112
+ cls: cls('title'),
113
+ text: card.title
114
+ });
115
+
116
+ // Description
117
+ if (card.description) {
118
+ const maxLen = 100;
119
+ const descText = card.description.length > maxLen
120
+ ? card.description.substring(0, maxLen) + '...'
121
+ : card.description;
122
+
123
+ items.push({
124
+ eztype: 'div',
125
+ cls: cls('description'),
126
+ text: descText
127
+ });
128
+ }
129
+
130
+ // Footer
131
+ const footerItems = this._buildFooterItems();
132
+ if (footerItems.length > 0) {
133
+ items.push({
134
+ eztype: 'div',
135
+ cls: cls('footer'),
136
+ items: footerItems
137
+ });
138
+ }
139
+
140
+ return items;
141
+ }
142
+
143
+ private _buildFooterItems(): unknown[] {
144
+ const { card, kanbanConfig } = this.config;
145
+ const items: unknown[] = [];
146
+
147
+ // Due date
148
+ if (kanbanConfig.showDueDate !== false && card.dueDate) {
149
+ const date = new Date(card.dueDate);
150
+ const isOverdue = date < new Date();
151
+
152
+ items.push({
153
+ eztype: 'span',
154
+ cls: isOverdue ? [cls('dueDate'), cls('overdue')].join(' ') : cls('dueDate'),
155
+ items: [
156
+ { eztype: 'i', cls: 'fa-regular fa-calendar' },
157
+ { eztype: 'span', text: this._formatDate(date) }
158
+ ]
159
+ });
160
+ }
161
+
162
+ // Checklist
163
+ if (kanbanConfig.showChecklist && card.checklist) {
164
+ items.push({
165
+ eztype: 'span',
166
+ cls: cls('meta'),
167
+ items: [
168
+ { eztype: 'i', cls: 'fa-regular fa-square-check' },
169
+ { eztype: 'span', text: `${card.checklist.completed}/${card.checklist.total}` }
170
+ ]
171
+ });
172
+ }
173
+
174
+ // Attachments
175
+ if (kanbanConfig.showAttachments && card.attachments) {
176
+ items.push({
177
+ eztype: 'span',
178
+ cls: cls('meta'),
179
+ items: [
180
+ { eztype: 'i', cls: 'fa-solid fa-paperclip' },
181
+ { eztype: 'span', text: String(card.attachments) }
182
+ ]
183
+ });
184
+ }
185
+
186
+ // Comments
187
+ if (kanbanConfig.showComments && card.comments) {
188
+ items.push({
189
+ eztype: 'span',
190
+ cls: cls('meta'),
191
+ items: [
192
+ { eztype: 'i', cls: 'fa-regular fa-comment' },
193
+ { eztype: 'span', text: String(card.comments) }
194
+ ]
195
+ });
196
+ }
197
+
198
+ // Assignees
199
+ if (kanbanConfig.showAssignee !== false) {
200
+ const assignees = card.assignees || (card.assignee ? [card.assignee] : []);
201
+
202
+ if (assignees.length > 0) {
203
+ // Spacer
204
+ items.push({
205
+ eztype: 'div',
206
+ style: { flex: '1' }
207
+ });
208
+
209
+ const avatarItems: unknown[] = assignees.slice(0, 3).map(assignee => {
210
+ if (assignee.avatar) {
211
+ return {
212
+ eztype: 'div',
213
+ cls: cls('avatar'),
214
+ title: assignee.name,
215
+ items: [{
216
+ eztype: 'img',
217
+ src: assignee.avatar,
218
+ alt: assignee.name
219
+ }]
220
+ };
221
+ } else {
222
+ return {
223
+ eztype: 'div',
224
+ cls: cls('avatar'),
225
+ title: assignee.name,
226
+ text: assignee.name.charAt(0).toUpperCase()
227
+ };
228
+ }
229
+ });
230
+
231
+ if (assignees.length > 3) {
232
+ avatarItems.push({
233
+ eztype: 'div',
234
+ cls: [cls('avatar'), cls('avatarMore')].join(' '),
235
+ text: `+${assignees.length - 3}`
236
+ });
237
+ }
238
+
239
+ items.push({
240
+ eztype: 'div',
241
+ cls: cls('assignees'),
242
+ items: avatarItems
243
+ });
244
+ }
245
+ }
246
+
247
+ return items;
248
+ }
249
+
250
+ private _formatDate(date: Date): string {
251
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
252
+ return `${months[date.getMonth()]} ${date.getDate()}`;
253
+ }
254
+
255
+ private _openEditDialog(): void {
256
+ const { card, column, kanbanController, kanbanConfig } = this.config;
257
+
258
+ ez.dialog.open({
259
+ title: 'Edit Card',
260
+ size: 'md',
261
+ items: [{
262
+ eztype: 'EzKanbanCardEditor',
263
+ card,
264
+ columnId: column._id,
265
+ availableLabels: kanbanConfig.availableLabels,
266
+ availableAssignees: kanbanConfig.availableAssignees,
267
+ onSave: (cardData: Partial<KanbanCard>) => {
268
+ kanbanController.updateCard(card.id, cardData);
269
+ }
270
+ }]
271
+ });
272
+ }
273
+ }
274
+
275
+ ez.define('EzKanbanCard', EzKanbanCard);
@@ -0,0 +1,209 @@
1
+ import { EzBaseComponent } from '../../EzBaseComponent.js';
2
+ import type { KanbanCard, KanbanLabel, KanbanAssignee, KanbanPriority } from '../EzKanbanTypes.js';
3
+
4
+ declare const ez: {
5
+ _createElement(config: unknown): Promise<HTMLElement>;
6
+ define(name: string, cls: unknown): void;
7
+ };
8
+
9
+ interface EzKanbanCardEditorConfig {
10
+ card?: KanbanCard;
11
+ columnId: string;
12
+ availableLabels?: KanbanLabel[];
13
+ availableAssignees?: KanbanAssignee[];
14
+ onSave?: (data: Partial<KanbanCard>) => void;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export class EzKanbanCardEditor extends EzBaseComponent {
19
+ declare config: EzKanbanCardEditorConfig;
20
+
21
+ private _formData: Partial<KanbanCard> = {};
22
+ private _selectedLabels: Set<string | number> = new Set();
23
+
24
+ async render(): Promise<HTMLElement> {
25
+ const { card, availableLabels, availableAssignees } = this.config;
26
+
27
+ this._formData = card ? { ...card } : {};
28
+ this._selectedLabels = new Set((card?.labels || []).map(l => l.id));
29
+
30
+ const items: unknown[] = [];
31
+
32
+ // Title input
33
+ items.push({
34
+ eztype: 'EzInput',
35
+ label: 'Title',
36
+ value: card?.title || '',
37
+ placeholder: 'Card title',
38
+ required: true,
39
+ onInput: (value: string) => {
40
+ this._formData.title = value;
41
+ }
42
+ });
43
+
44
+ // Description textarea
45
+ items.push({
46
+ eztype: 'EzTextarea',
47
+ label: 'Description',
48
+ value: card?.description || '',
49
+ placeholder: 'Add a description...',
50
+ rows: 3,
51
+ onInput: (value: string) => {
52
+ this._formData.description = value;
53
+ }
54
+ });
55
+
56
+ // Priority and Due Date row
57
+ items.push({
58
+ eztype: 'div',
59
+ style: { display: 'flex', gap: '16px' },
60
+ items: [
61
+ {
62
+ eztype: 'EzSelect',
63
+ label: 'Priority',
64
+ value: card?.priority || 'none',
65
+ options: [
66
+ { value: 'none', text: 'None' },
67
+ { value: 'low', text: 'Low' },
68
+ { value: 'medium', text: 'Medium' },
69
+ { value: 'high', text: 'High' },
70
+ { value: 'critical', text: 'Critical' }
71
+ ],
72
+ flex: 1,
73
+ onChange: (value: KanbanPriority) => {
74
+ this._formData.priority = value;
75
+ }
76
+ },
77
+ {
78
+ eztype: 'EzDatePicker',
79
+ label: 'Due Date',
80
+ value: card?.dueDate ? new Date(card.dueDate) : null,
81
+ flex: 1,
82
+ onChange: (value: Date | null) => {
83
+ this._formData.dueDate = value?.toISOString() || null;
84
+ }
85
+ }
86
+ ]
87
+ });
88
+
89
+ // Labels section
90
+ if (availableLabels?.length) {
91
+ items.push(await this._buildLabelsSection(availableLabels));
92
+ }
93
+
94
+ // Assignee select
95
+ if (availableAssignees?.length) {
96
+ items.push({
97
+ eztype: 'EzSelect',
98
+ label: 'Assignee',
99
+ value: card?.assignee?.id || '',
100
+ options: [
101
+ { value: '', text: 'Unassigned' },
102
+ ...availableAssignees.map(a => ({ value: a.id, text: a.name }))
103
+ ],
104
+ onChange: (value: string | number) => {
105
+ if (value) {
106
+ this._formData.assignee = availableAssignees.find(a => a.id === value) || null;
107
+ } else {
108
+ this._formData.assignee = null;
109
+ }
110
+ }
111
+ });
112
+ }
113
+
114
+ // Actions row
115
+ items.push({
116
+ eztype: 'div',
117
+ style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '8px' },
118
+ items: [{
119
+ eztype: 'EzButton',
120
+ text: card ? 'Save Changes' : 'Create Card',
121
+ variant: 'primary',
122
+ onClick: () => this._handleSave()
123
+ }]
124
+ });
125
+
126
+ const form = await ez._createElement({
127
+ eztype: 'div',
128
+ cls: 'ez-kanban-card-editor',
129
+ style: { display: 'flex', flexDirection: 'column', gap: '16px', padding: '8px' },
130
+ items
131
+ }) as HTMLElement;
132
+
133
+ return form;
134
+ }
135
+
136
+ private async _buildLabelsSection(availableLabels: KanbanLabel[]): Promise<unknown> {
137
+ const labelButtons = availableLabels.map(label => ({
138
+ eztype: 'button',
139
+ type: 'button',
140
+ style: {
141
+ padding: '4px 12px',
142
+ borderRadius: '4px',
143
+ border: `2px solid ${this._selectedLabels.has(label.id) ? label.color : 'transparent'}`,
144
+ background: label.color,
145
+ color: 'white',
146
+ fontSize: '12px',
147
+ cursor: 'pointer',
148
+ transition: 'border-color 0.15s'
149
+ },
150
+ text: label.text,
151
+ onClick: (e: Event) => {
152
+ const btn = e.currentTarget as HTMLButtonElement;
153
+ if (this._selectedLabels.has(label.id)) {
154
+ this._selectedLabels.delete(label.id);
155
+ btn.style.borderColor = 'transparent';
156
+ } else {
157
+ this._selectedLabels.add(label.id);
158
+ btn.style.borderColor = label.color;
159
+ }
160
+ this._formData.labels = availableLabels.filter(l => this._selectedLabels.has(l.id));
161
+ }
162
+ }));
163
+
164
+ return {
165
+ eztype: 'div',
166
+ items: [
167
+ {
168
+ eztype: 'label',
169
+ style: { fontSize: '13px', fontWeight: '500', marginBottom: '8px', display: 'block' },
170
+ text: 'Labels'
171
+ },
172
+ {
173
+ eztype: 'div',
174
+ style: { display: 'flex', flexWrap: 'wrap', gap: '8px' },
175
+ items: labelButtons
176
+ }
177
+ ]
178
+ };
179
+ }
180
+
181
+ private _handleSave(): void {
182
+ if (this._formData.title?.trim()) {
183
+ this.config.onSave?.(this._formData);
184
+ if (this.el) {
185
+ const dialog = this._findParentDialog(this.el);
186
+ if (dialog) {
187
+ (dialog as unknown as { close: () => void }).close();
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ private _findParentDialog(el: HTMLElement): HTMLElement | null {
194
+ let current: HTMLElement | null = el;
195
+ while (current) {
196
+ if (current.classList.contains('ez-dialog')) {
197
+ return current;
198
+ }
199
+ current = current.parentElement;
200
+ }
201
+ return null;
202
+ }
203
+
204
+ getData(): Partial<KanbanCard> {
205
+ return this._formData;
206
+ }
207
+ }
208
+
209
+ ez.define('EzKanbanCardEditor', EzKanbanCardEditor);