ngx-resource-scheduler 0.1.4 → 1.0.1

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/README.md CHANGED
@@ -36,12 +36,12 @@ npm install ngx-resource-scheduler
36
36
  ```ts
37
37
  startDate = new Date();
38
38
 
39
- resources = [
39
+ resources: SchedulerResource[] = [
40
40
  { id: 'r1', title: 'Room A' },
41
41
  { id: 'r2', title: 'Room B' },
42
42
  ];
43
43
 
44
- events = [
44
+ events: SchedulerEvent[] = [
45
45
  {
46
46
  id: 'e1',
47
47
  title: 'Meeting',
@@ -57,9 +57,9 @@ events = [
57
57
  ### Required
58
58
  | Input | Type | Type |
59
59
  | ------------- | ------------- | ------------- |
60
- | startDate | Date | First visible day (recommended at 00:00) |
61
- | resources | SchedulerResource[] | List of schedulable resources
62
- | events | SchedulerEvent[] | Events (UTC instants recommended)
60
+ | `startDate` | Date | First visible day (recommended at 00:00) |
61
+ | `resources` | SchedulerResource[] | List of schedulable resources
62
+ | `events` | SchedulerEvent[] | Events (UTC instants recommended)
63
63
 
64
64
  ### Layout & Range
65
65
 
@@ -81,11 +81,25 @@ events = [
81
81
  | `slotLineStyle` | `slot` | `slot`, `hour`, or `both` |
82
82
  | `readonly` | `false` | Disable interactions |
83
83
  | `timezone` | `local` | `local`, `UTC`, or IANA zone (e.g. `Europe/Kiev`) |
84
+ | `weekStartsOn` | `1` | First day of week. 0 = Sunday |
84
85
 
85
86
  > **Important**
86
87
  >
87
88
  > Events should be provided as UTC instants. The scheduler converts them for display using timezone.
88
89
 
90
+ ## i18n
91
+
92
+ | Input | Default | Description |
93
+ | ------------- | ------------- | ------------- |
94
+ | `showDaysResourcesLabel` | `true` | If the number of days/resources should be shown |
95
+ | `todayLabel` | `Today` | Your translation for "Today" |
96
+ | `daysLabel` | `days` | Your translation for "days" |
97
+ | `resourcesLabel` | `resources` | Your translation for "resources" |
98
+ | `prevLabel` | `<` | Your translation for "<" |
99
+ | `nextLabel` | `>` | Your translation for ">" |
100
+ | `locale` | `null` | Locale to be used in the dates header |
101
+
102
+
89
103
  ## 🎯 Outputs
90
104
 
91
105
  | Output | Payload | Description |
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/ngx-resource-scheduler",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngx-resource-scheduler",
3
- "version": "0.1.4",
3
+ "version": "1.0.1",
4
4
  "description": "Angular scheduler with date columns and nested resource columns or inverted",
5
5
  "keywords": [
6
6
  "angular",
@@ -22,16 +22,5 @@
22
22
  "dependencies": {
23
23
  "tslib": "^2.3.0"
24
24
  },
25
- "sideEffects": false,
26
- "module": "fesm2022/ngx-resource-scheduler.mjs",
27
- "typings": "types/ngx-resource-scheduler.d.ts",
28
- "exports": {
29
- "./package.json": {
30
- "default": "./package.json"
31
- },
32
- ".": {
33
- "types": "./types/ngx-resource-scheduler.d.ts",
34
- "default": "./fesm2022/ngx-resource-scheduler.mjs"
35
- }
36
- }
37
- }
25
+ "sideEffects": false
26
+ }
@@ -0,0 +1,16 @@
1
+ import { SchedulerEvent } from "../types";
2
+
3
+ /**
4
+ * INTERNAL type
5
+ * Represents a unique scheduler grid cell.
6
+ * Not exported in public-api.ts.
7
+ */
8
+ export interface CellKey {
9
+ day: Date;
10
+ resourceId: string;
11
+ }
12
+
13
+ export type PositionedEvent = SchedulerEvent & {
14
+ _col: number;
15
+ _cols: number; // total columns in its overlap group
16
+ };
@@ -0,0 +1,131 @@
1
+ <div class="ngx-scheduler">
2
+
3
+ <!-- Toolbar -->
4
+ @if (showToolbar) {
5
+ <div class="ngx-toolbar">
6
+ <div class="ngx-toolbar-left">
7
+ <button type="button" class="ngx-btn" (click)="navigatePrev()">{{ prevLabel }}</button>
8
+ <button type="button" class="ngx-btn ngx-btn--ghost" (click)="navigateToday()">{{ todayLabel }}</button>
9
+ <button type="button" class="ngx-btn" (click)="navigateNext()">{{ nextLabel }}</button>
10
+ </div>
11
+ <div class="ngx-toolbar-title">
12
+ {{ rangeTitle }}
13
+ </div>
14
+ <div class="ngx-toolbar-right">
15
+ @if (showDaysResourcesLabel) {
16
+ <span class="ngx-toolbar-meta">
17
+ {{ primaryAxis === 'days' ? (visibleDays.length + ' ' + daysLabel) : (resources.length + ' ' + resourcesLabel ) }}
18
+ </span>
19
+ }
20
+ @if (!showDaysResourcesLabel) {
21
+ <span class="ngx-toolbar-meta">&nbsp;</span>
22
+ }
23
+ </div>
24
+ </div>
25
+ }
26
+
27
+ <!-- Header row -->
28
+ <div class="ngx-header">
29
+ <div class="ngx-time-gutter"></div>
30
+
31
+ <div class="ngx-primary-headers" [style.gridTemplateColumns]="'repeat(' + primaryColumns.length + ', minmax(0, 1fr))'">
32
+ @for (p of primaryColumns; track trackPrimary($index, p)) {
33
+ <div class="ngx-primary-header">
34
+ <div class="ngx-primary-title-row">
35
+ <div class="ngx-primary-title">{{ p.title }}</div>
36
+ </div>
37
+ <div class="ngx-secondary-header-row" [style.gridTemplateColumns]="'repeat(' + secondaryColumns.length + ', minmax(0, 1fr))'">
38
+ @for (s of secondaryColumns; track trackSecondary($index, s)) {
39
+ <div class="ngx-secondary-header">
40
+ <span class="ngx-secondary-header-title">{{ s.title }}</span>
41
+ </div>
42
+ }
43
+ </div>
44
+ </div>
45
+ }
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Body -->
50
+ <div class="ngx-body">
51
+ <!-- time gutter -->
52
+ <div class="ngx-time-gutter" [style.height.px]="timelineHeightPx">
53
+ @for (h of hourLabels; track h) {
54
+ <div
55
+ class="ngx-hour-label"
56
+ [style.top.px]="hourTopPx(h)">
57
+ {{ formatHour(h) }}
58
+ </div>
59
+ }
60
+ </div>
61
+
62
+
63
+ <!-- grid -->
64
+ <div class="ngx-grid"
65
+ [style.gridTemplateColumns]="'repeat(' + primaryColumns.length + ', minmax(0, 1fr))'"
66
+ [style.height.px]="timelineHeightPx">
67
+
68
+ @for (p of primaryColumns; track trackPrimary($index, p)) {
69
+ <div class="ngx-primary-col">
70
+ <div class="ngx-secondary-cols"
71
+ [style.gridTemplateColumns]="'repeat(' + secondaryColumns.length + ', minmax(0, 1fr))'"
72
+ [style.height.px]="timelineHeightPx">
73
+ <!-- Each cell is (day, resource) regardless of axis order -->
74
+ @for (s of secondaryColumns; track trackSecondary($index, s)) {
75
+ <div class="ngx-cell"
76
+ #cellEl
77
+ [style.height.px]="timelineHeightPx"
78
+ (click)="cellClick(p, s, $event)"
79
+ [attr.data-day]="cellDayKey(p,s)"
80
+ [attr.data-resource]="cellResourceId(p,s)">
81
+ <!-- grid lines -->
82
+ <div class="ngx-lines" aria-hidden="true">
83
+ <!-- slot lines -->
84
+ @for (line of slotLines; track line) {
85
+ <div
86
+ class="ngx-line ngx-line--slot"
87
+ [style.top.px]="line.top"
88
+ [class.ngx-line--half]="line.isHalfHour"
89
+ [class.is-hidden]="slotLineStyle === 'hour'">
90
+ </div>
91
+ }
92
+ <!-- hour lines -->
93
+ @for (top of hourLineOffsetsPx; track top) {
94
+ <div
95
+ class="ngx-line ngx-line--hour"
96
+ [style.top.px]="top"
97
+ [class.is-hidden]="slotLineStyle === 'slot'">
98
+ </div>
99
+ }
100
+ </div>
101
+ <!-- events -->
102
+ <!-- ngClass and 2nd ngStyle are only used when user passes custom class/style -->
103
+ @for (e of cellEvents(p, s); track trackEvent($index, e)) {
104
+ <div class="ngx-event"
105
+ [ngStyle]="getEventLayoutStyle(e, p, s)"
106
+ (click)="onEventClick(e, $event); $event.stopPropagation()"
107
+ [ngClass]="eventClass ? eventClass(e) : null"
108
+ [attr.title]="eventTooltip(e)">
109
+ @if (eventTemplate) {
110
+ <ng-container
111
+ [ngTemplateOutlet]="eventTemplate"
112
+ [ngTemplateOutletContext]="eventTemplateCtx(e, p, s)">
113
+ </ng-container>
114
+ } @else {
115
+ <div class="ngx-event-title">{{ e.title }}</div>
116
+ <div class="ngx-event-time">
117
+ {{ toDisplayZone(e.start) | date:'HH:mm' }}–{{ toDisplayZone(e.end) | date:'HH:mm' }}
118
+ </div>
119
+ }
120
+ </div>
121
+ }
122
+ </div>
123
+ }
124
+ </div>
125
+ </div>
126
+ }
127
+
128
+ </div>
129
+ </div>
130
+
131
+ </div>
@@ -0,0 +1,443 @@
1
+ :host {
2
+ display: block;
3
+
4
+ /* Theme tokens */
5
+ --ngx-bg: #ffffff;
6
+ --ngx-surface: #ffffff;
7
+ --ngx-muted: #f6f7f9;
8
+ --ngx-border: rgba(16, 24, 40, 0.10);
9
+ --ngx-border-soft: rgba(16, 24, 40, 0.06);
10
+ --ngx-text: #101828;
11
+ --ngx-text-muted: rgba(16, 24, 40, 0.60);
12
+
13
+ --ngx-radius: 6px;
14
+
15
+ --ngx-shadow: 0 10px 25px rgba(16, 24, 40, 0.06);
16
+ --ngx-shadow-soft: 0 8px 18px rgba(16, 24, 40, 0.05);
17
+
18
+ /* Events */
19
+ --ngx-event-bg: #eef4ff;
20
+ --ngx-event-border: rgba(53, 122, 246, 0.25);
21
+ --ngx-event-text: #0b1f44;
22
+
23
+ /* Sizing */
24
+ --ngx-time-gutter-width: 72px;
25
+ --ngx-header-height: 78px; // includes primary + secondary headers
26
+ --ngx-primary-title-height: 40px;
27
+ --ngx-secondary-title-height: 45px;
28
+
29
+ /* Layout */
30
+ background: var(--ngx-bg);
31
+ color: var(--ngx-text);
32
+ --ngx-grid-gap: 5px;
33
+
34
+ /* Better font rendering */
35
+ -webkit-font-smoothing: antialiased;
36
+ -moz-osx-font-smoothing: grayscale;
37
+
38
+ font-weight: 400;
39
+ font-family:
40
+ -apple-system,
41
+ BlinkMacSystemFont,
42
+ "Segoe UI",
43
+ Roboto,
44
+ Inter,
45
+ Helvetica,
46
+ Arial,
47
+ sans-serif,
48
+ "Apple Color Emoji",
49
+ "Segoe UI Emoji";
50
+ }
51
+
52
+ .ngx-scheduler {
53
+ display: grid;
54
+ grid-template-rows: auto 1fr;
55
+ gap: var(--ngx-grid-gap);
56
+ }
57
+
58
+ /* ===== Toolbar ===== */
59
+ .ngx-toolbar {
60
+ display: grid;
61
+ grid-template-columns: auto 1fr auto;
62
+ align-items: center;
63
+ gap: 12px;
64
+
65
+ padding: 10px 10px 2px 10px;
66
+ }
67
+
68
+ .ngx-toolbar-title {
69
+ text-align: center;
70
+ font-weight: 650;
71
+ font-size: 14px;
72
+ color: var(--ngx-text);
73
+ letter-spacing: 0.2px;
74
+ }
75
+
76
+ .ngx-toolbar-meta {
77
+ font-size: 12px;
78
+ color: var(--ngx-text-muted);
79
+ padding-right: 6px;
80
+ }
81
+
82
+ .ngx-toolbar-left {
83
+ display: flex;
84
+ gap: 8px;
85
+ align-items: center;
86
+ }
87
+
88
+ .ngx-btn {
89
+ height: 34px;
90
+ padding: 0 10px;
91
+ border-radius: 12px;
92
+
93
+ border: 1px solid var(--ngx-border);
94
+ background: #fff;
95
+ color: var(--ngx-text);
96
+
97
+ font-weight: 600;
98
+ font-size: 12px;
99
+ cursor: pointer;
100
+
101
+ box-shadow: 0 8px 14px rgba(16, 24, 40, 0.06);
102
+ transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease;
103
+ }
104
+
105
+ .ngx-btn:hover {
106
+ transform: translateY(-1px);
107
+ box-shadow: 0 12px 18px rgba(16, 24, 40, 0.10);
108
+ }
109
+
110
+ .ngx-btn:active {
111
+ transform: translateY(0);
112
+ box-shadow: 0 8px 14px rgba(16, 24, 40, 0.08);
113
+ }
114
+
115
+ .ngx-btn--ghost {
116
+ background: var(--ngx-muted);
117
+ }
118
+
119
+ /* ===== Header ===== */
120
+ .ngx-header {
121
+ position: sticky;
122
+ top: 0;
123
+ z-index: 5;
124
+ backdrop-filter: blur(8px);
125
+ gap: var(--ngx-grid-gap);
126
+
127
+ display: grid;
128
+ grid-template-columns: var(--ngx-time-gutter-width) 1fr;
129
+ align-items: stretch;
130
+
131
+ background: var(--ngx-bg);
132
+ }
133
+
134
+ /* empty corner */
135
+ .ngx-header .ngx-time-gutter {
136
+ background: transparent;
137
+ }
138
+
139
+
140
+
141
+ /* Primary column headers (days or resources) */
142
+ .ngx-primary-headers {
143
+ display: grid;
144
+ gap: var(--ngx-grid-gap);
145
+ }
146
+
147
+ .ngx-primary-header {
148
+ border: 1px solid var(--ngx-border);
149
+ border-radius: var(--ngx-radius);
150
+ overflow: hidden;
151
+ background: var(--ngx-surface);
152
+ box-shadow: var(--ngx-shadow-soft);
153
+ }
154
+
155
+ /* Primary title line */
156
+ .ngx-primary-title-row {
157
+ height: var(--ngx-primary-title-height);
158
+ display: flex;
159
+ align-items: center;
160
+ padding: 0 12px;
161
+
162
+ font-weight: 650;
163
+ font-size: 13px;
164
+ letter-spacing: 0.2px;
165
+
166
+ border-bottom: 1px solid var(--ngx-border-soft);
167
+ background: linear-gradient(to bottom, #ffffff, #fbfbfc);
168
+
169
+ .ngx-primary-title {
170
+ margin: auto;
171
+ text-align: center;
172
+ }
173
+ }
174
+
175
+
176
+ /* Secondary header row (resources or days) */
177
+ .ngx-secondary-header-row {
178
+ height: var(--ngx-secondary-title-height);
179
+ display: grid;
180
+ }
181
+
182
+ .ngx-secondary-header {
183
+ display: flex;
184
+ align-items: center;
185
+
186
+ padding: 0 6px;
187
+
188
+ font-size: 12px;
189
+ text-align: center;
190
+ color: var(--ngx-text-muted);
191
+
192
+ border-left: 1px solid var(--ngx-border-soft);
193
+ background: #ffffff;
194
+ }
195
+
196
+ .ngx-secondary-header-title {
197
+ margin: auto;
198
+ }
199
+
200
+ .ngx-secondary-header:first-child {
201
+ border-left: none;
202
+ }
203
+
204
+ /* ===== Body ===== */
205
+ .ngx-body {
206
+ display: grid;
207
+ grid-template-columns: var(--ngx-time-gutter-width) 1fr;
208
+ gap: 10px;
209
+
210
+ /* scrolling area */
211
+ min-height: 320px;
212
+ }
213
+
214
+ .ngx-time-gutter {
215
+ position: relative;
216
+ user-select: none;
217
+ }
218
+
219
+ .ngx-hour-label {
220
+ position: absolute;
221
+ left: 0;
222
+ right: 8px;
223
+
224
+ transform: translateY(-50%);
225
+ text-align: right;
226
+
227
+ font-size: 12px;
228
+ color: var(--ngx-text-muted);
229
+ pointer-events: none;
230
+ }
231
+
232
+ /* Special cases so first and last labels are visible and not clipped */
233
+ .ngx-hour-label:first-child {
234
+ transform: translateY(0);
235
+ top: 0 !important;
236
+ }
237
+ .ngx-hour-label:last-child {
238
+ transform: translateY(-100%);
239
+ }
240
+
241
+ /* Grid with primary columns */
242
+ .ngx-grid {
243
+ position: relative;
244
+ display: grid;
245
+ gap: var(--ngx-grid-gap);
246
+ }
247
+
248
+ /* Each primary column looks like a card */
249
+ .ngx-primary-col {
250
+ border: 1px solid var(--ngx-border);
251
+ border-radius: var(--ngx-radius);
252
+ overflow: hidden;
253
+ background: var(--ngx-surface);
254
+ box-shadow: var(--ngx-shadow);
255
+ }
256
+
257
+ /* Secondary columns container inside each primary column */
258
+ .ngx-secondary-cols {
259
+ display: grid;
260
+ height: 100%;
261
+ }
262
+
263
+ /* Individual day/resource cell */
264
+ .ngx-cell {
265
+ position: relative;
266
+ border-left: 1px solid var(--ngx-border-soft);
267
+ background: #ffffff;
268
+
269
+ /* Click affordance */
270
+ cursor: pointer;
271
+ transition: background 120ms ease;
272
+ }
273
+ .ngx-cell .ngx-event {
274
+ box-sizing: border-box;
275
+ }
276
+
277
+ .ngx-cell:first-child {
278
+ border-left: none;
279
+ }
280
+
281
+ .ngx-cell:hover {
282
+ background: rgba(16, 24, 40, 0.02);
283
+ }
284
+
285
+ /* Grid lines inside each cell */
286
+ .ngx-lines {
287
+ position: absolute;
288
+ inset: 0;
289
+ pointer-events: none;
290
+ }
291
+
292
+ .ngx-line {
293
+ position: absolute;
294
+ left: 0;
295
+ right: 0;
296
+ height: 0;
297
+ border-top: 1px solid var(--ngx-border-soft);
298
+ opacity: 0.75;
299
+ }
300
+
301
+ /* Slot lines are subtler */
302
+ .ngx-line--slot {
303
+ opacity: 0.5;
304
+ }
305
+
306
+ /* Slightly stronger at hh:30 */
307
+ .ngx-line--half {
308
+ opacity: 0.7;
309
+ border-top-color: rgba(16, 24, 40, 0.09);
310
+ }
311
+
312
+ /* Hour lines are stronger */
313
+ .ngx-line--hour {
314
+ opacity: 0.9;
315
+ border-top-color: rgba(16, 24, 40, 0.10);
316
+ }
317
+
318
+ .is-hidden {
319
+ display: none;
320
+ }
321
+
322
+ /* ===== Events ===== */
323
+ .ngx-event {
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: 2px;
327
+ touch-action: none; /* for pointer events */
328
+ user-select: none;
329
+
330
+ min-width: 0; /* critical so children can shrink */
331
+ overflow: hidden; /* keep content inside rounded card */
332
+
333
+ position: absolute;
334
+ /* left/width are computed in TS for overlaps */
335
+ min-width: 0;
336
+ margin: 0;
337
+
338
+ border-radius: 4px;
339
+ background: var(--ngx-event-bg);
340
+ border: 1px solid var(--ngx-event-border);
341
+ color: var(--ngx-event-text);
342
+
343
+ padding: 2px 4px;
344
+ box-sizing: border-box;
345
+
346
+ box-shadow: 0 10px 18px rgba(16, 24, 40, 0.08);
347
+ cursor: pointer;
348
+
349
+ transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
350
+ }
351
+
352
+
353
+ .ngx-event:hover {
354
+ transform: translateY(-1px);
355
+ box-shadow: 0 14px 22px rgba(16, 24, 40, 0.12);
356
+ filter: saturate(1.05);
357
+ }
358
+
359
+ .ngx-event:hover .ngx-resize {
360
+ opacity: 1;
361
+ }
362
+
363
+ .ngx-event:active {
364
+ transform: translateY(0px);
365
+ box-shadow: 0 10px 18px rgba(16, 24, 40, 0.10);
366
+ cursor: grabbing;
367
+ }
368
+
369
+ /* Text inside event */
370
+ .ngx-event-title {
371
+ font-weight: 650;
372
+ font-size: 11px;
373
+ line-height: 1.2;
374
+
375
+ white-space: nowrap;
376
+ overflow: hidden;
377
+ text-overflow: ellipsis;
378
+ }
379
+
380
+ .ngx-event-time {
381
+ min-width: 0; /* critical */
382
+ overflow: hidden;
383
+ text-overflow: ellipsis;
384
+ white-space: nowrap;
385
+ margin-top: 3px;
386
+ font-size: 10px;
387
+ color: rgba(11, 31, 68, 0.72);
388
+ }
389
+
390
+ /* FONT SETUP */
391
+ .ngx-event-time,
392
+ .ngx-toolbar-meta,
393
+ .ngx-secondary-header {
394
+ letter-spacing: 0.01em;
395
+ }
396
+
397
+ .ngx-hour,
398
+ .ngx-event-time {
399
+ font-variant-numeric: tabular-nums;
400
+ }
401
+
402
+ /* ===== Smooth scrollbars (optional) ===== */
403
+ .ngx-body {
404
+ overflow: auto;
405
+ padding-bottom: 4px;
406
+ gap: var(--ngx-grid-gap);
407
+ }
408
+
409
+ /* Webkit scrollbar styling (Chrome/Safari/Edge) */
410
+ .ngx-body::-webkit-scrollbar {
411
+ width: 10px;
412
+ height: 10px;
413
+ }
414
+ .ngx-body::-webkit-scrollbar-thumb {
415
+ background: rgba(16, 24, 40, 0.14);
416
+ border-radius: 10px;
417
+ border: 2px solid rgba(255, 255, 255, 0.9);
418
+ }
419
+ .ngx-body::-webkit-scrollbar-track {
420
+ background: rgba(16, 24, 40, 0.04);
421
+ border-radius: 10px;
422
+ }
423
+
424
+ /* ===== Responsive tweaks ===== */
425
+ @media (max-width: 900px) {
426
+ :host {
427
+ --ngx-time-gutter-width: 58px;
428
+ }
429
+ .ngx-primary-headers,
430
+ .ngx-grid {
431
+ display: grid;
432
+ gap: 10px;
433
+ }
434
+ .ngx-primary-headers {
435
+ padding-left: calc(var(--ngx-grid-gap) / 2);
436
+ padding-right: calc(var(--ngx-grid-gap) / 2);
437
+ }
438
+ .ngx-event {
439
+ left: 6px;
440
+ right: 6px;
441
+ border-radius: 10px;
442
+ }
443
+ }