@operato/gantt 2.0.0-beta.5 → 7.0.0-rc.10

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 (47) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/src/gantt-chart-controller.d.ts +32 -0
  3. package/dist/src/gantt-chart-controller.js +395 -0
  4. package/dist/src/gantt-chart-controller.js.map +1 -0
  5. package/dist/src/locales.d.ts +52 -0
  6. package/dist/src/locales.js +79 -0
  7. package/dist/src/locales.js.map +1 -0
  8. package/dist/src/ox-gantt-chart.d.ts +11 -20
  9. package/dist/src/ox-gantt-chart.js +141 -435
  10. package/dist/src/ox-gantt-chart.js.map +1 -1
  11. package/dist/src/ox-gantt-resize-splitter.d.ts +2 -3
  12. package/dist/src/ox-gantt-resize-splitter.js +15 -22
  13. package/dist/src/ox-gantt-resize-splitter.js.map +1 -1
  14. package/dist/src/ox-gantt.d.ts +17 -3
  15. package/dist/src/ox-gantt.js +169 -29
  16. package/dist/src/ox-gantt.js.map +1 -1
  17. package/dist/src/ox-task-tooltip.d.ts +12 -0
  18. package/dist/src/ox-task-tooltip.js +132 -0
  19. package/dist/src/ox-task-tooltip.js.map +1 -0
  20. package/dist/src/types.d.ts +11 -4
  21. package/dist/src/types.js.map +1 -1
  22. package/dist/src/utils.d.ts +3 -3
  23. package/dist/src/utils.js.map +1 -1
  24. package/dist/stories/ox-gantt.stories.d.ts +19 -2
  25. package/dist/stories/ox-gantt.stories.js +104 -90
  26. package/dist/stories/ox-gantt.stories.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +7 -4
  29. package/src/gantt-chart-controller.ts +473 -0
  30. package/src/locales.ts +78 -0
  31. package/src/ox-gantt-chart.ts +151 -484
  32. package/src/ox-gantt-resize-splitter.ts +18 -25
  33. package/src/ox-gantt.ts +174 -31
  34. package/src/ox-task-tooltip.ts +120 -0
  35. package/src/types.ts +13 -5
  36. package/src/utils.ts +4 -4
  37. package/stories/ox-gantt.stories.ts +118 -91
  38. package/themes/grist-theme.css +1 -3
  39. package/translations/en.json +11 -1
  40. package/translations/ja.json +11 -1
  41. package/translations/ko.json +11 -1
  42. package/translations/ms.json +11 -1
  43. package/translations/zh.json +11 -1
  44. package/dist/src/locale.d.ts +0 -1
  45. package/dist/src/locale.js +0 -49
  46. package/dist/src/locale.js.map +0 -1
  47. package/src/locale.ts +0 -51
@@ -1,5 +1,4 @@
1
1
  import { LitElement, css, html } from 'lit'
2
-
3
2
  import { customElement } from 'lit/decorators.js'
4
3
 
5
4
  @customElement('ox-gantt-resize-splitter')
@@ -39,20 +38,7 @@ export class OxGanttResizeSplitter extends LitElement {
39
38
  }
40
39
 
41
40
  render() {
42
- return html` <div @mousedown=${(e: MouseEvent) => this.onMouseDown(e)}></div> `
43
- }
44
-
45
- // TODO onDrag 이벤트가 계속 발생하므로 처리하는 성능 저하됨. 그래서 throttling 하도록 함
46
- _throttled(delay: number, fn: (...args: any) => void) {
47
- let lastCall = 0
48
- return function (...args: any) {
49
- const now = new Date().getTime()
50
- if (now - lastCall < delay) {
51
- return
52
- }
53
- lastCall = now
54
- return fn(...args)
55
- }
41
+ return html`<div @mousedown=${(e: MouseEvent) => this.onMouseDown(e)}></div>`
56
42
  }
57
43
 
58
44
  private onMouseDown(e: MouseEvent) {
@@ -68,20 +54,33 @@ export class OxGanttResizeSplitter extends LitElement {
68
54
  })
69
55
  )
70
56
 
57
+ this.parentElement!.addEventListener('mousemove', this.onMouseMoveHandler)
58
+ this.parentElement!.addEventListener('mouseup', this.onMouseUpHandler)
71
59
  window.addEventListener('mousemove', this.onMouseMoveHandler)
72
60
  window.addEventListener('mouseup', this.onMouseUpHandler)
73
61
 
74
62
  e.stopPropagation()
75
63
  }
76
64
 
77
- onMouseMoveHandler = this._throttled(100, this.onMouseMove.bind(this)) // this.onMouseMove.bind(this)
78
- onMouseUpHandler = this.onMouseUp.bind(this)
65
+ private onMouseMoveHandler = (e: MouseEvent) => this.onMouseMove(e)
66
+ private onMouseUpHandler = (e: MouseEvent) => this.onMouseUp(e)
79
67
 
80
68
  private onMouseMove(e: MouseEvent) {
81
69
  if (!this.dragstart) {
82
70
  return
83
71
  }
84
72
 
73
+ const parentRect = this.parentElement!.getBoundingClientRect()
74
+ if (
75
+ e.clientX < parentRect.left ||
76
+ e.clientX > parentRect.right ||
77
+ e.clientY < parentRect.top ||
78
+ e.clientY > parentRect.bottom
79
+ ) {
80
+ this.onMouseUp(e)
81
+ return
82
+ }
83
+
85
84
  e.preventDefault()
86
85
 
87
86
  this.dispatchEvent(
@@ -94,9 +93,6 @@ export class OxGanttResizeSplitter extends LitElement {
94
93
  }
95
94
  })
96
95
  )
97
- requestAnimationFrame(() => {
98
- dispatchEvent(new Event('resize'))
99
- })
100
96
 
101
97
  if (this.hasAttribute('vertical')) {
102
98
  document.body.style.cursor = 'col-resize'
@@ -110,6 +106,8 @@ export class OxGanttResizeSplitter extends LitElement {
110
106
  }
111
107
 
112
108
  private onMouseUp(e: MouseEvent) {
109
+ this.parentElement!.removeEventListener('mousemove', this.onMouseMoveHandler)
110
+ this.parentElement!.removeEventListener('mouseup', this.onMouseUpHandler)
113
111
  window.removeEventListener('mousemove', this.onMouseMoveHandler)
114
112
  window.removeEventListener('mouseup', this.onMouseUpHandler)
115
113
 
@@ -124,11 +122,6 @@ export class OxGanttResizeSplitter extends LitElement {
124
122
  })
125
123
  )
126
124
 
127
- requestAnimationFrame(() => {
128
- dispatchEvent(new Event('resize'))
129
- delete this.dragstart
130
- })
131
-
132
125
  document.body.style.cursor = ''
133
126
  document.body.style.userSelect = ''
134
127
 
package/src/ox-gantt.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { LitElement, html, css, PropertyValues, PropertyValueMap } from 'lit'
2
- import { customElement, property, query } from 'lit/decorators.js'
2
+ import i18next from 'i18next'
3
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
4
  import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
4
5
  import { ScrollbarStyles } from '@operato/styles'
5
6
 
6
7
  import './ox-gantt-chart.js'
7
8
  import './ox-gantt-resize-splitter.js'
8
9
  import { OxGanttChart } from './ox-gantt-chart.js'
9
- import { Task } from './types.js'
10
+ import { ColumnConfig, Task, TimeScale } from './types.js'
10
11
 
11
12
  @customElement('ox-gantt')
12
13
  export class OxGantt extends LitElement {
@@ -18,6 +19,7 @@ export class OxGantt extends LitElement {
18
19
  display: block;
19
20
  font-family: Arial, sans-serif;
20
21
  text-wrap: nowrap;
22
+ position: relative;
21
23
  }
22
24
 
23
25
  .container {
@@ -44,6 +46,7 @@ export class OxGantt extends LitElement {
44
46
 
45
47
  ox-gantt-resize-splitter {
46
48
  width: 4px;
49
+ cursor: ew-resize;
47
50
  }
48
51
 
49
52
  table {
@@ -59,6 +62,9 @@ export class OxGantt extends LitElement {
59
62
  top: -1px;
60
63
  z-index: 1;
61
64
  background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
65
+ border-color: var(--ox-gantt-header-border-color, var(--md-sys-color-on-secondary-container));
66
+ color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
67
+ text-transform: capitalize;
62
68
  }
63
69
 
64
70
  th,
@@ -77,6 +83,10 @@ export class OxGantt extends LitElement {
77
83
  height: 30px;
78
84
  box-sizing: border-box;
79
85
  }
86
+
87
+ tr.selected {
88
+ background-color: var(--ox-gantt-selected-background-color, var(--md-sys-color-surface-variant));
89
+ }
80
90
  `
81
91
  ]
82
92
 
@@ -85,6 +95,51 @@ export class OxGantt extends LitElement {
85
95
  fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] /* beginning of this year */
86
96
  @property({ type: String, attribute: 'to-date' })
87
97
  toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0] /* end of this year */
98
+ @property({ type: Boolean, attribute: 'extend-grid-lines' }) extendGridLines: boolean = false
99
+ @property({ type: String, attribute: 'time-scale' }) timeScale: TimeScale = 'week-day'
100
+ @property({ type: Array }) columnConfig: ColumnConfig[] = [
101
+ { name: 'id', label: i18next.t('label.gantt-task-id') || 'ID', visible: true, width: 50, order: 1 },
102
+ {
103
+ name: 'section',
104
+ label: i18next.t('label.gantt-task-section') || 'section',
105
+ visible: true,
106
+ width: 100,
107
+ order: 2
108
+ },
109
+ { name: 'title', label: i18next.t('label.gantt-task-title') || 'title', visible: true, width: 150, order: 3 },
110
+ { name: 'type', label: i18next.t('label.gantt-task-type') || 'type', visible: true, width: 100, order: 4 },
111
+ {
112
+ name: 'startDate',
113
+ label: i18next.t('label.gantt-task-start-date') || 'start date',
114
+ visible: true,
115
+ width: 100,
116
+ order: 5
117
+ },
118
+ {
119
+ name: 'endDate',
120
+ label: i18next.t('label.gantt-task-end-date') || 'end date',
121
+ visible: true,
122
+ width: 100,
123
+ order: 6
124
+ },
125
+ { name: 'tags', label: i18next.t('label.gantt-task-tags') || 'tags', visible: true, width: 100, order: 7 },
126
+ {
127
+ name: 'progress',
128
+ label: i18next.t('label.gantt-task-progress') || 'progress',
129
+ visible: true,
130
+ width: 100,
131
+ order: 8
132
+ },
133
+ {
134
+ name: 'dependsOn',
135
+ label: i18next.t('label.gantt-task-previous') || 'previous',
136
+ visible: true,
137
+ width: 100,
138
+ order: 9
139
+ }
140
+ ]
141
+
142
+ @state() selectedTaskId?: string
88
143
 
89
144
  @query('.container') container!: HTMLDivElement
90
145
  @query('.task-container') taskContainer!: HTMLDivElement
@@ -93,6 +148,7 @@ export class OxGantt extends LitElement {
93
148
  @query('ox-gantt-chart') ganttChart!: OxGanttChart
94
149
 
95
150
  private _startWidth: number = 0
151
+ private _isDragging: boolean = false
96
152
 
97
153
  render() {
98
154
  return html`
@@ -102,6 +158,7 @@ export class OxGantt extends LitElement {
102
158
  vertical
103
159
  @splitter-dragstart=${(e: CustomEvent) => this.resizeStart(e)}
104
160
  @splitter-drag=${(e: CustomEvent) => this.resizeDrag(e)}
161
+ @splitter-dragend=${(e: CustomEvent) => this.resizeEnd(e)}
105
162
  ></ox-gantt-resize-splitter>
106
163
  <ox-gantt-chart
107
164
  class="chart-container"
@@ -109,42 +166,48 @@ export class OxGantt extends LitElement {
109
166
  .fromDate=${this.fromDate}
110
167
  .toDate=${this.toDate}
111
168
  .tasks=${this.tasks}
112
- axis-scale="week-day"
113
- extend-grid-lines
169
+ .selectedTaskId=${this.selectedTaskId}
170
+ .axisScale=${this.timeScale}
171
+ ?extend-grid-lines=${this.extendGridLines}
172
+ @task-clicked=${this.handleTaskClicked}
114
173
  ></ox-gantt-chart>
115
174
  </div>
116
175
  `
117
176
  }
118
177
 
119
178
  renderTaskTable() {
179
+ if (!this.columnConfig) {
180
+ return
181
+ }
182
+
183
+ const mergedTasks = this.mergeTasksBySection(this.tasks)
184
+
185
+ // Sort columns by the configured order
186
+ const sortedColumns = [...this.columnConfig].sort((a, b) => a.order - b.order)
187
+
120
188
  return html`
121
189
  <table>
122
190
  <thead>
123
191
  <tr>
124
- <th>ID</th>
125
- <th>Section</th>
126
- <th>Title</th>
127
- <th>Type</th>
128
- <th>Start Time</th>
129
- <th>End Time</th>
130
- <th>Tags</th>
131
- <th>Progress</th>
132
- <th>Prev. Task</th>
192
+ ${sortedColumns.map(col =>
193
+ col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` : null
194
+ )}
133
195
  </tr>
134
196
  </thead>
135
197
  <tbody>
136
- ${this.tasks.map(
137
- task => html`
138
- <tr>
139
- <td>${task.id}</td>
140
- <td>${task.section?.name || ''}</td>
141
- <td>${task.title}</td>
142
- <td>${task.type}</td>
143
- <td>${task.startTime.toISOString().split('T')[0]}</td>
144
- <td>${task.endTime.toISOString().split('T')[0]}</td>
145
- <td>${task.tags?.join(', ') || ''}</td>
146
- <td>${task.progress || ''}</td>
147
- <td>${task.after || ''}</td>
198
+ ${mergedTasks.map(
199
+ ({ task, showSection, rowSpan }) => html`
200
+ <tr class="${this.selectedTaskId === task.id ? 'selected' : ''}" @click=${() => this.selectTask(task.id)}>
201
+ ${sortedColumns.map(col => {
202
+ if (col.name === 'section') {
203
+ return col.visible && showSection
204
+ ? html`<td rowspan=${rowSpan} style="width: ${col.width}px">${task.section?.name || ''}</td>`
205
+ : null
206
+ }
207
+ return col.visible
208
+ ? html`<td style="width: ${col.width}px">${this.getTaskFieldValue(task, col.name)}</td>`
209
+ : null
210
+ })}
148
211
  </tr>
149
212
  `
150
213
  )}
@@ -153,25 +216,105 @@ export class OxGantt extends LitElement {
153
216
  `
154
217
  }
155
218
 
219
+ getTaskFieldValue(task: Task, field: string) {
220
+ switch (field) {
221
+ case 'id':
222
+ return task.id
223
+ case 'section':
224
+ return task.section?.name || ''
225
+ case 'title':
226
+ return task.title
227
+ case 'type':
228
+ return task.type
229
+ case 'startDate':
230
+ return new Date(task.startDate).toISOString().split('T')[0]
231
+ case 'endDate':
232
+ return new Date(task.endDate).toISOString().split('T')[0]
233
+ case 'tags':
234
+ return task.tags?.join(', ') || ''
235
+ case 'progress':
236
+ return task.progress || ''
237
+ case 'dependsOn':
238
+ return task.dependsOn || ''
239
+ default:
240
+ return ''
241
+ }
242
+ }
243
+
244
+ mergeTasksBySection(tasks: Task[]): Array<{ task: Task; showSection: boolean; rowSpan: number }> {
245
+ const mergedTasks = [] as Array<{ task: Task; showSection: boolean; rowSpan: number }>
246
+ let currentSection = ''
247
+ let rowSpan = 0
248
+
249
+ ;(tasks || []).forEach((task, index) => {
250
+ if (task.section?.name !== currentSection) {
251
+ currentSection = task.section?.name || ''
252
+ rowSpan = 1
253
+ mergedTasks.push({ task, showSection: true, rowSpan: 1 })
254
+ } else {
255
+ mergedTasks[mergedTasks.length - rowSpan].rowSpan = ++rowSpan
256
+ mergedTasks.push({ task, showSection: false, rowSpan: 0 })
257
+ }
258
+
259
+ if (index === tasks.length - 1 && rowSpan > 0) {
260
+ mergedTasks[mergedTasks.length - rowSpan].rowSpan = rowSpan
261
+ }
262
+ })
263
+
264
+ return mergedTasks
265
+ }
266
+
267
+ selectTask(taskId: string) {
268
+ this.selectedTaskId = taskId
269
+ }
270
+
271
+ handleTaskClicked(event: CustomEvent) {
272
+ this.selectedTaskId = event.detail.taskId
273
+ }
274
+
156
275
  protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
157
276
  this.taskContainer.style.width = '254px'
158
277
  }
159
278
 
160
279
  updated(changes: PropertyValues<this>) {}
161
280
 
162
- syncScroll() {
163
- this.ganttChart.setVerticalOffset(this.taskContainer.scrollTop)
281
+ syncScroll(e: Event) {
282
+ const target = e.target as HTMLElement
283
+ if (target === this.taskContainer) {
284
+ // Sync chart scroll with table scroll
285
+ const newScrollTop = this.taskContainer.scrollTop
286
+ this.ganttChart.setVerticalOffset(newScrollTop)
287
+
288
+ // Ensure chart scroll matches table scroll
289
+ if (this.ganttChart.getVerticalOffset() !== newScrollTop) {
290
+ this.ganttChart.setVerticalOffset(newScrollTop)
291
+ }
292
+ } else if (target === this.ganttChart) {
293
+ // Sync table scroll with chart scroll
294
+ const newScrollTop = this.ganttChart.getVerticalOffset()
295
+ this.taskContainer.scrollTop = newScrollTop
296
+
297
+ // Ensure table scroll matches chart scroll
298
+ this.ganttChart.setVerticalOffset(this.taskContainer.scrollTop)
299
+ }
164
300
  }
165
301
 
166
302
  resizeStart(e: CustomEvent) {
167
303
  this._startWidth = ((e.target as HTMLElement)?.previousElementSibling as HTMLElement)?.offsetWidth
304
+ this._isDragging = true
168
305
  }
169
306
 
170
307
  resizeDrag(e: CustomEvent) {
171
- var delta = e.detail
172
- var x = (e.target as HTMLElement)?.previousElementSibling as HTMLElement
173
- if (x) {
174
- x.style.width = `${this._startWidth + delta.x}px`
308
+ if (!this._isDragging) return
309
+ const delta = e.detail
310
+ const taskContainer = this.taskContainer
311
+
312
+ if (taskContainer) {
313
+ taskContainer.style.width = `${this._startWidth + delta.x}px`
175
314
  }
176
315
  }
316
+
317
+ resizeEnd(e: CustomEvent) {
318
+ this._isDragging = false
319
+ }
177
320
  }
@@ -0,0 +1,120 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+
4
+ @customElement('ox-task-tooltip')
5
+ export class OxTaskTooltip extends LitElement {
6
+ static styles = css`
7
+ :host {
8
+ position: fixed; /* position: absolute로 변경 */
9
+ background-color: var(--ox-gantt-tooltip-background-color, var(--md-sys-color-surface, #f9f9f9));
10
+ border: 1px solid var(--ox-gantt-tooltip-border-color, var(--md-sys-color-outline, #ccc));
11
+ padding: var(--spacing-medium, 10px);
12
+ border-radius: var(--spacing-medium, 8px);
13
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
14
+ visibility: hidden;
15
+ z-index: 10;
16
+ max-width: 300px;
17
+ font-family: Roboto;
18
+ font-size: 14px;
19
+ line-height: 1.6;
20
+ color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
21
+ transition:
22
+ visibility 0.1s,
23
+ opacity 0.1s;
24
+ opacity: 0; /* 초기에는 보이지 않음 */
25
+ }
26
+ :host([visible]) {
27
+ visibility: visible;
28
+ opacity: 1; /* 보일 때는 불투명 */
29
+ }
30
+ .title {
31
+ font-weight: bold;
32
+ font-size: 16px;
33
+ margin-bottom: 8px;
34
+ color: var(--ox-gantt-tooltip-title-color, var(--md-sys-color-primary, #0078d4)); /* 파란색 텍스트 */
35
+ }
36
+ .detail {
37
+ display: grid;
38
+ grid-template-columns: auto auto 1fr;
39
+ gap: 4px;
40
+ align-items: center;
41
+ }
42
+ .label {
43
+ font-weight: bold;
44
+ color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface-variant, #333));
45
+ text-align: right; /* 라벨을 우측 정렬 */
46
+ }
47
+ .colon {
48
+ text-align: center;
49
+ color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
50
+ }
51
+ .value {
52
+ text-align: left; /* 값을 좌측 정렬 */
53
+ color: var(--ox-gantt-tooltip-color, var(--md-sys-color-on-surface, #333));
54
+ }
55
+ `
56
+
57
+ @property({ type: String }) title = ''
58
+ @property({ type: String }) startDate = ''
59
+ @property({ type: String }) endDate = ''
60
+ @property({ type: Number }) progress = 0
61
+ @property({ type: String }) type = ''
62
+
63
+ show(x: number, y: number) {
64
+ const tooltipWidth = this.offsetWidth
65
+ const tooltipHeight = this.offsetHeight
66
+ const offsetX = 10
67
+ const offsetY = 10
68
+
69
+ // 기본 위치 계산
70
+ let left = x + offsetX
71
+ let top = y + offsetY
72
+
73
+ // 화면 경계 확인 및 조정
74
+ const screenWidth = window.innerWidth
75
+ const screenHeight = window.innerHeight
76
+
77
+ if (left + tooltipWidth > screenWidth) {
78
+ left = x - tooltipWidth - offsetX
79
+ }
80
+ if (top + tooltipHeight > screenHeight) {
81
+ top = y - tooltipHeight - offsetY
82
+ }
83
+
84
+ // offsetParent 요소를 기준으로 위치 설정
85
+ if (this.offsetParent) {
86
+ const parentRect = this.offsetParent!.getBoundingClientRect()
87
+ this.style.left = `${left - parentRect.left}px`
88
+ this.style.top = `${top - parentRect.top}px`
89
+ } else {
90
+ this.style.left = `${left}px`
91
+ this.style.top = `${top}px`
92
+
93
+ const myRect = this.getBoundingClientRect()
94
+
95
+ /* 실제 바운드와 설정한 left/top 이 일치하지 않는다면, 부모 어딘가에서 transform 되었다고 판단하고, 보정해줌 */
96
+ if (myRect.left !== left || myRect.top !== top) {
97
+ this.style.left = `${left + (left - myRect.left)}px`
98
+ this.style.top = `${top + (top - myRect.top)}px`
99
+ }
100
+ }
101
+
102
+ this.setAttribute('visible', '')
103
+ }
104
+
105
+ hide() {
106
+ this.removeAttribute('visible')
107
+ }
108
+
109
+ render() {
110
+ return html`
111
+ <div class="title">${this.title}</div>
112
+ <div class="detail">
113
+ <span class="label">Type</span><span class="colon">:</span><span class="value">${this.type}</span>
114
+ <span class="label">Start Date</span><span class="colon">:</span><span class="value">${this.startDate}</span>
115
+ <span class="label">End Date</span><span class="colon">:</span><span class="value">${this.endDate}</span>
116
+ <span class="label">Progress</span><span class="colon">:</span><span class="value">${this.progress}%</span>
117
+ </div>
118
+ `
119
+ }
120
+ }
package/src/types.ts CHANGED
@@ -6,11 +6,11 @@ export interface Task {
6
6
  title: string
7
7
  type: TaskType
8
8
  section?: Section
9
- startTime: Date
10
- endTime: Date
9
+ startDate: Date | string
10
+ endDate: Date | string
11
11
  progress?: number
12
- after?: string // 이전 태스크의 ID
13
- length: string // 예: '3d' (3 days)
12
+ dependsOn?: string
13
+ length: string
14
14
  tags?: TaskTag[]
15
15
  children?: Task[]
16
16
  }
@@ -19,4 +19,12 @@ export interface Section {
19
19
  name: string
20
20
  }
21
21
 
22
- export type LEVEL = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
22
+ export interface ColumnConfig {
23
+ name: string
24
+ label: string
25
+ visible: boolean
26
+ width: number
27
+ order: number
28
+ }
29
+
30
+ export type TimeScale = 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'
package/src/utils.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { LEVEL } from './types'
1
+ import { TimeScale } from './types'
2
2
 
3
- const levels: LEVEL[] = ['week-day', 'quarter-week', 'year-month', 'year-quarter']
3
+ const levels: TimeScale[] = ['week-day', 'quarter-week', 'year-month', 'year-quarter']
4
4
 
5
- export function levelUp(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): LEVEL {
5
+ export function levelUp(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): TimeScale {
6
6
  const current = Math.min(levels.length, levels.indexOf(level) + 1)
7
7
  return levels[current]
8
8
  }
9
9
 
10
- export function levelDown(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): LEVEL {
10
+ export function levelDown(level: 'year-quarter' | 'year-month' | 'quarter-week' | 'week-day'): TimeScale {
11
11
  const current = Math.max(0, levels.indexOf(level) - 1)
12
12
  return levels[current]
13
13
  }