@happyvertical/smrt-projects 0.30.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.
Files changed (92) hide show
  1. package/AGENTS.md +31 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +97 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/Issues.d.ts +107 -0
  8. package/dist/collections/Issues.d.ts.map +1 -0
  9. package/dist/collections/Projects.d.ts +90 -0
  10. package/dist/collections/Projects.d.ts.map +1 -0
  11. package/dist/collections/PullRequests.d.ts +107 -0
  12. package/dist/collections/PullRequests.d.ts.map +1 -0
  13. package/dist/collections/Repositories.d.ts +77 -0
  14. package/dist/collections/Repositories.d.ts.map +1 -0
  15. package/dist/constants.d.ts +9 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/index.d.ts +14 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +2477 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manifest.json +4193 -0
  22. package/dist/models/Comment.d.ts +77 -0
  23. package/dist/models/Comment.d.ts.map +1 -0
  24. package/dist/models/Issue.d.ts +200 -0
  25. package/dist/models/Issue.d.ts.map +1 -0
  26. package/dist/models/Label.d.ts +63 -0
  27. package/dist/models/Label.d.ts.map +1 -0
  28. package/dist/models/Project.d.ts +183 -0
  29. package/dist/models/Project.d.ts.map +1 -0
  30. package/dist/models/PullRequest.d.ts +114 -0
  31. package/dist/models/PullRequest.d.ts.map +1 -0
  32. package/dist/models/Repository.d.ts +141 -0
  33. package/dist/models/Repository.d.ts.map +1 -0
  34. package/dist/playground.d.ts +2 -0
  35. package/dist/playground.d.ts.map +1 -0
  36. package/dist/playground.js +129 -0
  37. package/dist/playground.js.map +1 -0
  38. package/dist/prompts.d.ts +2 -0
  39. package/dist/prompts.d.ts.map +1 -0
  40. package/dist/smrt-knowledge.json +1956 -0
  41. package/dist/svelte/components/ApprovalActions.svelte +213 -0
  42. package/dist/svelte/components/ApprovalActions.svelte.d.ts +17 -0
  43. package/dist/svelte/components/ApprovalActions.svelte.d.ts.map +1 -0
  44. package/dist/svelte/components/BulkActions.svelte +224 -0
  45. package/dist/svelte/components/BulkActions.svelte.d.ts +14 -0
  46. package/dist/svelte/components/BulkActions.svelte.d.ts.map +1 -0
  47. package/dist/svelte/components/DurationDisplay.svelte +68 -0
  48. package/dist/svelte/components/DurationDisplay.svelte.d.ts +11 -0
  49. package/dist/svelte/components/DurationDisplay.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/RejectDialog.svelte +250 -0
  51. package/dist/svelte/components/RejectDialog.svelte.d.ts +17 -0
  52. package/dist/svelte/components/RejectDialog.svelte.d.ts.map +1 -0
  53. package/dist/svelte/components/TimeEntryCard.svelte +294 -0
  54. package/dist/svelte/components/TimeEntryCard.svelte.d.ts +17 -0
  55. package/dist/svelte/components/TimeEntryCard.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/TimeEntryList.svelte +351 -0
  57. package/dist/svelte/components/TimeEntryList.svelte.d.ts +17 -0
  58. package/dist/svelte/components/TimeEntryList.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/TimeSummary.svelte +165 -0
  60. package/dist/svelte/components/TimeSummary.svelte.d.ts +19 -0
  61. package/dist/svelte/components/TimeSummary.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/__tests__/ApprovalActions.test.js +41 -0
  63. package/dist/svelte/components/__tests__/BulkActions.test.js +46 -0
  64. package/dist/svelte/components/__tests__/DurationDisplay.test.js +23 -0
  65. package/dist/svelte/components/__tests__/RejectDialog.test.js +45 -0
  66. package/dist/svelte/components/__tests__/TimeEntryCard.test.js +45 -0
  67. package/dist/svelte/components/__tests__/TimeEntryList.test.js +50 -0
  68. package/dist/svelte/components/__tests__/TimeSummary.test.js +39 -0
  69. package/dist/svelte/components/utils.d.ts +42 -0
  70. package/dist/svelte/components/utils.d.ts.map +1 -0
  71. package/dist/svelte/components/utils.js +43 -0
  72. package/dist/svelte/i18n.d.ts +18 -0
  73. package/dist/svelte/i18n.d.ts.map +1 -0
  74. package/dist/svelte/i18n.js +18 -0
  75. package/dist/svelte/index.d.ts +26 -0
  76. package/dist/svelte/index.d.ts.map +1 -0
  77. package/dist/svelte/index.js +31 -0
  78. package/dist/svelte/playground.d.ts +122 -0
  79. package/dist/svelte/playground.d.ts.map +1 -0
  80. package/dist/svelte/playground.js +114 -0
  81. package/dist/svelte/utils.d.ts +42 -0
  82. package/dist/svelte/utils.d.ts.map +1 -0
  83. package/dist/svelte/utils.js +43 -0
  84. package/dist/types.d.ts +54 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/dist/types.js +2 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/ui.d.ts +10 -0
  89. package/dist/ui.d.ts.map +1 -0
  90. package/dist/ui.js +85 -0
  91. package/dist/ui.js.map +1 -0
  92. package/package.json +100 -0
@@ -0,0 +1,351 @@
1
+ <script lang="ts">
2
+ /**
3
+ * TimeEntryList - List of time entries with optional selection
4
+ * Supports bulk selection and grouping
5
+ */
6
+
7
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
+ import { M } from '../i18n.js';
9
+ import {
10
+ type Currency,
11
+ formatCurrency,
12
+ formatDate,
13
+ formatHours,
14
+ statusColors,
15
+ type TimeEntry,
16
+ } from './utils.js';
17
+
18
+ const { t } = useI18n();
19
+
20
+ /** Props for TimeEntryList component */
21
+ export interface Props {
22
+ entries: TimeEntry[];
23
+ selectable?: boolean;
24
+ selectedIds?: string[];
25
+ onselectionchange?: (ids: string[]) => void;
26
+ emptyMessage?: string;
27
+ baseHref?: string;
28
+ currency?: Currency;
29
+ /** Filter function to determine which entries can be selected */
30
+ canSelect?: (entry: TimeEntry) => boolean;
31
+ }
32
+
33
+ let {
34
+ entries,
35
+ selectable = false,
36
+ selectedIds = [],
37
+ onselectionchange,
38
+ emptyMessage = 'No time entries',
39
+ baseHref,
40
+ currency = 'CAD',
41
+ canSelect = () => true,
42
+ }: Props = $props();
43
+
44
+ // Filter entries that can actually be selected
45
+ const selectableEntries = $derived(selectable ? entries.filter(canSelect) : []);
46
+
47
+ const allSelected = $derived(
48
+ selectableEntries.length > 0 &&
49
+ selectableEntries.every((e) => selectedIds.includes(e.id)),
50
+ );
51
+
52
+ const someSelected = $derived(selectedIds.length > 0 && !allSelected);
53
+
54
+ function handleSelectAll(event: Event) {
55
+ const target = event.target as HTMLInputElement;
56
+ if (target.checked) {
57
+ onselectionchange?.(selectableEntries.map((e) => e.id));
58
+ } else {
59
+ onselectionchange?.([]);
60
+ }
61
+ }
62
+
63
+ function handleSelect(id: string, selected: boolean, event: Event) {
64
+ event.preventDefault();
65
+ event.stopPropagation();
66
+ if (selected) {
67
+ onselectionchange?.([...selectedIds, id]);
68
+ } else {
69
+ onselectionchange?.(selectedIds.filter((i) => i !== id));
70
+ }
71
+ }
72
+
73
+ function getEntryHref(entry: TimeEntry): string | undefined {
74
+ if (!baseHref) return undefined;
75
+ return `${baseHref}/${entry.id}`;
76
+ }
77
+ </script>
78
+
79
+ {#if entries.length === 0}
80
+ <div class="empty-state">
81
+ <p>{emptyMessage}</p>
82
+ </div>
83
+ {:else}
84
+ <div class="time-entry-list">
85
+ {#if selectable && selectableEntries.length > 0}
86
+ <div class="list-header">
87
+ <label class="select-all">
88
+ <input
89
+ type="checkbox"
90
+ checked={allSelected}
91
+ indeterminate={someSelected}
92
+ onchange={handleSelectAll}
93
+ aria-label={t(M['projects.time_entry_list.select_all_aria'])}
94
+ />
95
+ <span>{t(M['projects.time_entry_list.select_all'])}</span>
96
+ </label>
97
+ <span class="selection-count">
98
+ {t(M['projects.time_entry_list.selection_count'], { selected: selectedIds.length, total: selectableEntries.length })}
99
+ </span>
100
+ </div>
101
+ {/if}
102
+
103
+ <div class="entries">
104
+ {#each entries as entry (entry.id)}
105
+ {@const isSelected = selectedIds.includes(entry.id)}
106
+ {@const isSelectable = selectable && canSelect(entry)}
107
+ {@const entryHref = getEntryHref(entry)}
108
+ <div
109
+ class="entry-row"
110
+ class:selected={isSelected}
111
+ class:selectable={isSelectable}
112
+ class:has-checkbox={selectable && selectableEntries.length > 0}
113
+ >
114
+ {#if selectable && selectableEntries.length > 0}
115
+ <div class="checkbox-cell">
116
+ {#if isSelectable}
117
+ <input
118
+ type="checkbox"
119
+ checked={isSelected}
120
+ onchange={(e) => handleSelect(entry.id, (e.target as HTMLInputElement).checked, e)}
121
+ aria-label={t(M['projects.time_entry_list.select_entry_aria'], { description: entry.description })}
122
+ />
123
+ {/if}
124
+ </div>
125
+ {/if}
126
+
127
+ {#if entryHref}
128
+ <a href={entryHref} class="entry-content">
129
+ {@render entryDetails(entry)}
130
+ </a>
131
+ {:else}
132
+ <div class="entry-content">
133
+ {@render entryDetails(entry)}
134
+ </div>
135
+ {/if}
136
+ </div>
137
+ {/each}
138
+ </div>
139
+ </div>
140
+ {/if}
141
+
142
+ {#snippet entryDetails(entry: TimeEntry)}
143
+ <div class="date-cell">
144
+ {formatDate(entry.date)}
145
+ </div>
146
+
147
+ <div class="description-cell">
148
+ <span class="description">{entry.description}</span>
149
+ {#if entry.workerName}
150
+ <span class="worker">{entry.workerName}</span>
151
+ {/if}
152
+ </div>
153
+
154
+ <div class="hours-cell">
155
+ {formatHours(entry.hours)}
156
+ </div>
157
+
158
+ <div class="status-cell">
159
+ <span class="status-badge" style="--status-color: {statusColors[entry.status]}">
160
+ {entry.status}
161
+ </span>
162
+ </div>
163
+
164
+ {#if entry.amount !== undefined}
165
+ <div class="amount-cell">
166
+ {formatCurrency(entry.amount, currency)}
167
+ </div>
168
+ {/if}
169
+ {/snippet}
170
+
171
+ <style>
172
+ .empty-state {
173
+ padding: 3rem 1rem;
174
+ text-align: center;
175
+ color: var(--smrt-color-on-surface-variant);
176
+ }
177
+
178
+ .time-entry-list {
179
+ background: var(--smrt-color-surface);
180
+ border-radius: var(--smrt-radius-large, 16px);
181
+ overflow: hidden;
182
+ border: 1px solid var(--smrt-color-outline-variant);
183
+ }
184
+
185
+ .list-header {
186
+ display: flex;
187
+ justify-content: space-between;
188
+ align-items: center;
189
+ padding: 0.75rem 1rem;
190
+ background: var(--smrt-color-surface-container-low);
191
+ border-bottom: 1px solid var(--smrt-color-outline-variant);
192
+ }
193
+
194
+ .select-all {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 0.5rem;
198
+ cursor: pointer;
199
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
200
+ font-weight: var(--smrt-typography-label-large-weight, 500);
201
+ }
202
+
203
+ .select-all input[type='checkbox'] {
204
+ width: 1rem;
205
+ height: 1rem;
206
+ cursor: pointer;
207
+ accent-color: var(--smrt-color-primary);
208
+ }
209
+
210
+ .selection-count {
211
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
212
+ color: var(--smrt-color-on-surface-variant);
213
+ }
214
+
215
+ .entries {
216
+ display: flex;
217
+ flex-direction: column;
218
+ }
219
+
220
+ .entry-row {
221
+ display: flex;
222
+ align-items: center;
223
+ border-bottom: 1px solid var(--smrt-color-outline-variant);
224
+ transition: background 0.15s var(--smrt-easing-standard);
225
+ }
226
+
227
+ .entry-row:last-child {
228
+ border-bottom: none;
229
+ }
230
+
231
+ .entry-row:hover {
232
+ background: var(--smrt-color-surface-container-lowest);
233
+ }
234
+
235
+ .entry-row.selected {
236
+ background: var(--smrt-color-primary-container);
237
+ }
238
+
239
+ .checkbox-cell {
240
+ width: 2rem;
241
+ flex-shrink: 0;
242
+ padding-left: 1rem;
243
+ }
244
+
245
+ .checkbox-cell input[type='checkbox'] {
246
+ width: 1rem;
247
+ height: 1rem;
248
+ cursor: pointer;
249
+ accent-color: var(--smrt-color-primary);
250
+ }
251
+
252
+ .entry-content {
253
+ display: flex;
254
+ align-items: center;
255
+ flex: 1;
256
+ padding: 0.875rem 1rem;
257
+ text-decoration: none;
258
+ color: inherit;
259
+ min-width: 0;
260
+ }
261
+
262
+ a.entry-content:focus {
263
+ outline: 2px solid var(--smrt-color-primary);
264
+ outline-offset: -2px;
265
+ }
266
+
267
+ .date-cell {
268
+ width: 4.5rem;
269
+ flex-shrink: 0;
270
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
271
+ color: var(--smrt-color-on-surface-variant);
272
+ }
273
+
274
+ .description-cell {
275
+ flex: 1;
276
+ min-width: 0;
277
+ padding-right: 1rem;
278
+ }
279
+
280
+ .description {
281
+ display: block;
282
+ font-size: var(--smrt-typography-body-large-size, 0.9375rem);
283
+ color: var(--smrt-color-on-surface);
284
+ white-space: nowrap;
285
+ overflow: hidden;
286
+ text-overflow: ellipsis;
287
+ }
288
+
289
+ .worker {
290
+ display: block;
291
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
292
+ color: var(--smrt-color-on-surface-variant);
293
+ margin-top: 0.125rem;
294
+ }
295
+
296
+ .hours-cell {
297
+ width: 4rem;
298
+ flex-shrink: 0;
299
+ font-size: var(--smrt-typography-body-large-size, 0.9375rem);
300
+ font-weight: var(--smrt-typography-title-medium-weight, 500);
301
+ color: var(--smrt-color-primary);
302
+ text-align: right;
303
+ padding-right: 1rem;
304
+ }
305
+
306
+ .status-cell {
307
+ width: 6rem;
308
+ flex-shrink: 0;
309
+ text-align: center;
310
+ }
311
+
312
+ .status-badge {
313
+ display: inline-block;
314
+ font-size: var(--smrt-typography-label-small-size, 0.625rem);
315
+ font-weight: var(--smrt-typography-label-small-weight, 500);
316
+ padding: 0.25rem 0.5rem;
317
+ border-radius: var(--smrt-radius-small, 8px);
318
+ background: var(--status-color);
319
+ color: var(--smrt-color-on-primary);
320
+ text-transform: uppercase;
321
+ letter-spacing: var(--smrt-typography-label-small-tracking, 0.5px);
322
+ }
323
+
324
+ .amount-cell {
325
+ width: 6rem;
326
+ flex-shrink: 0;
327
+ font-size: var(--smrt-typography-body-large-size, 0.9375rem);
328
+ font-weight: var(--smrt-typography-title-medium-weight, 500);
329
+ text-align: right;
330
+ }
331
+
332
+ @media (max-width: 640px) {
333
+ .entry-content {
334
+ flex-wrap: wrap;
335
+ gap: 0.5rem;
336
+ }
337
+
338
+ .description-cell {
339
+ order: 1;
340
+ width: 100%;
341
+ padding-right: 0;
342
+ }
343
+
344
+ .date-cell,
345
+ .hours-cell,
346
+ .status-cell,
347
+ .amount-cell {
348
+ flex: none;
349
+ }
350
+ }
351
+ </style>
@@ -0,0 +1,17 @@
1
+ import { type Currency, type TimeEntry } from './utils.js';
2
+ /** Props for TimeEntryList component */
3
+ export interface Props {
4
+ entries: TimeEntry[];
5
+ selectable?: boolean;
6
+ selectedIds?: string[];
7
+ onselectionchange?: (ids: string[]) => void;
8
+ emptyMessage?: string;
9
+ baseHref?: string;
10
+ currency?: Currency;
11
+ /** Filter function to determine which entries can be selected */
12
+ canSelect?: (entry: TimeEntry) => boolean;
13
+ }
14
+ declare const TimeEntryList: import("svelte").Component<Props, {}, "">;
15
+ type TimeEntryList = ReturnType<typeof TimeEntryList>;
16
+ export default TimeEntryList;
17
+ //# sourceMappingURL=TimeEntryList.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeEntryList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TimeEntryList.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,QAAQ,EAKb,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAGpB,wCAAwC;AACxC,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,iEAAiE;IACjE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;CAC3C;AAyID,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,165 @@
1
+ <script lang="ts">
2
+ /**
3
+ * TimeSummary - Summary statistics for time entries
4
+ * Shows total hours, amounts, and pending items
5
+ */
6
+
7
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
+ import { M } from '../i18n.js';
9
+ import { type Currency, formatCurrency, formatHours } from './utils.js';
10
+
11
+ /** Props for TimeSummary component */
12
+ export interface Props {
13
+ totalHours: number;
14
+ totalAmount: number;
15
+ pendingHours?: number;
16
+ pendingAmount?: number;
17
+ approvedHours?: number;
18
+ approvedAmount?: number;
19
+ entryCount?: number;
20
+ currency?: Currency;
21
+ showPending?: boolean;
22
+ showApproved?: boolean;
23
+ layout?: 'horizontal' | 'grid';
24
+ }
25
+
26
+ let {
27
+ totalHours,
28
+ totalAmount,
29
+ pendingHours = 0,
30
+ pendingAmount = 0,
31
+ approvedHours = 0,
32
+ approvedAmount = 0,
33
+ entryCount,
34
+ currency = 'CAD',
35
+ showPending = true,
36
+ showApproved = false,
37
+ layout = 'grid',
38
+ }: Props = $props();
39
+
40
+ const { t } = useI18n();
41
+ </script>
42
+
43
+ <div class="time-summary" class:horizontal={layout === 'horizontal'}>
44
+ <div class="summary-card">
45
+ <span class="label">{t(M['projects.time_summary.total_hours'])}</span>
46
+ <span class="value">{formatHours(totalHours)}</span>
47
+ {#if entryCount !== undefined}
48
+ <span class="count">{entryCount} {entryCount === 1 ? 'entry' : 'entries'}</span>
49
+ {/if}
50
+ </div>
51
+
52
+ <div class="summary-card">
53
+ <span class="label">{t(M['projects.time_summary.total_value'])}</span>
54
+ <span class="value">{formatCurrency(totalAmount, currency)}</span>
55
+ </div>
56
+
57
+ {#if showPending && (pendingHours > 0 || pendingAmount > 0)}
58
+ <div class="summary-card highlight">
59
+ <span class="label">{t(M['projects.time_summary.pending_approval'])}</span>
60
+ <span class="value">{formatHours(pendingHours)}</span>
61
+ <span class="sub-value">{formatCurrency(pendingAmount, currency)}</span>
62
+ </div>
63
+ {/if}
64
+
65
+ {#if showApproved && (approvedHours > 0 || approvedAmount > 0)}
66
+ <div class="summary-card success">
67
+ <span class="label">Approved</span>
68
+ <span class="value">{formatHours(approvedHours)}</span>
69
+ <span class="sub-value">{formatCurrency(approvedAmount, currency)}</span>
70
+ </div>
71
+ {/if}
72
+ </div>
73
+
74
+ <style>
75
+ .time-summary {
76
+ display: grid;
77
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
78
+ gap: 1rem;
79
+ }
80
+
81
+ .time-summary.horizontal {
82
+ display: flex;
83
+ flex-wrap: wrap;
84
+ gap: 1rem;
85
+ }
86
+
87
+ .time-summary.horizontal .summary-card {
88
+ flex: 1 1 140px;
89
+ min-width: 140px;
90
+ }
91
+
92
+ .summary-card {
93
+ background: var(--smrt-color-surface);
94
+ border: 1px solid var(--smrt-color-outline-variant);
95
+ border-radius: var(--smrt-radius-medium, 12px);
96
+ padding: 1rem;
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.25rem;
100
+ }
101
+
102
+ .summary-card.highlight {
103
+ background: var(--smrt-color-tertiary-container);
104
+ border-color: var(--smrt-color-tertiary);
105
+ }
106
+
107
+ .summary-card.success {
108
+ background: var(--smrt-color-primary-container);
109
+ border-color: var(--smrt-color-primary);
110
+ }
111
+
112
+ .label {
113
+ font-size: var(--smrt-typography-label-small-size, 0.75rem);
114
+ font-weight: var(--smrt-typography-label-small-weight, 500);
115
+ color: var(--smrt-color-on-surface-variant);
116
+ text-transform: uppercase;
117
+ letter-spacing: var(--smrt-typography-label-small-tracking, 0.5px);
118
+ }
119
+
120
+ .value {
121
+ font-size: var(--smrt-typography-headline-small-size, 1.5rem);
122
+ font-weight: var(--smrt-typography-headline-small-weight, 400);
123
+ color: var(--smrt-color-on-surface);
124
+ }
125
+
126
+ .sub-value {
127
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
128
+ color: var(--smrt-color-on-surface-variant);
129
+ }
130
+
131
+ .count {
132
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
133
+ color: var(--smrt-color-on-surface-variant);
134
+ }
135
+
136
+ .highlight .label {
137
+ color: var(--smrt-color-on-tertiary-container);
138
+ }
139
+
140
+ .highlight .value {
141
+ color: var(--smrt-color-tertiary);
142
+ }
143
+
144
+ .highlight .sub-value {
145
+ color: var(--smrt-color-on-tertiary-container);
146
+ }
147
+
148
+ .success .label {
149
+ color: var(--smrt-color-on-primary-container);
150
+ }
151
+
152
+ .success .value {
153
+ color: var(--smrt-color-primary);
154
+ }
155
+
156
+ .success .sub-value {
157
+ color: var(--smrt-color-on-primary-container);
158
+ }
159
+
160
+ @media (max-width: 480px) {
161
+ .time-summary {
162
+ grid-template-columns: 1fr 1fr;
163
+ }
164
+ }
165
+ </style>
@@ -0,0 +1,19 @@
1
+ import { type Currency } from './utils.js';
2
+ /** Props for TimeSummary component */
3
+ export interface Props {
4
+ totalHours: number;
5
+ totalAmount: number;
6
+ pendingHours?: number;
7
+ pendingAmount?: number;
8
+ approvedHours?: number;
9
+ approvedAmount?: number;
10
+ entryCount?: number;
11
+ currency?: Currency;
12
+ showPending?: boolean;
13
+ showApproved?: boolean;
14
+ layout?: 'horizontal' | 'grid';
15
+ }
16
+ declare const TimeSummary: import("svelte").Component<Props, {}, "">;
17
+ type TimeSummary = ReturnType<typeof TimeSummary>;
18
+ export default TimeSummary;
19
+ //# sourceMappingURL=TimeSummary.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeSummary.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TimeSummary.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,YAAY,CAAC;AAGxE,sCAAsC;AACtC,MAAM,WAAW,KAAK;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;CAChC;AA6DD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,41 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for ApprovalActions via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import ApprovalActions from '../ApprovalActions.svelte';
8
+ describe('ApprovalActions', () => {
9
+ it('offers Submit/Delete for a draft and submits on click', async () => {
10
+ const onsubmit = vi.fn();
11
+ render(ApprovalActions, {
12
+ props: { status: 'draft', onsubmit, ondelete: vi.fn() },
13
+ });
14
+ expect(screen.getByRole('button', { name: 'Submit for Approval' })).toBeInTheDocument();
15
+ await userEvent.click(screen.getByRole('button', { name: 'Submit for Approval' }));
16
+ expect(onsubmit).toHaveBeenCalledTimes(1);
17
+ });
18
+ it('offers Approve/Reject for a submitted entry', async () => {
19
+ const onapprove = vi.fn();
20
+ render(ApprovalActions, {
21
+ props: { status: 'submitted', onapprove, onreject: vi.fn() },
22
+ });
23
+ await userEvent.click(screen.getByRole('button', { name: 'Approve' }));
24
+ expect(onapprove).toHaveBeenCalledTimes(1);
25
+ expect(screen.getByRole('button', { name: 'Reject' })).toBeInTheDocument();
26
+ });
27
+ it('shows the approved status message', () => {
28
+ render(ApprovalActions, { props: { status: 'approved' } });
29
+ expect(screen.getByText('This entry has been approved')).toBeInTheDocument();
30
+ });
31
+ it('is axe-clean', async () => {
32
+ const { container } = render(ApprovalActions, {
33
+ props: {
34
+ status: 'submitted',
35
+ onapprove: vi.fn(),
36
+ onreject: vi.fn(),
37
+ },
38
+ });
39
+ await expectNoA11yViolations(container);
40
+ });
41
+ });
@@ -0,0 +1,46 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for BulkActions via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import BulkActions from '../BulkActions.svelte';
8
+ const baseProps = (over = {}) => ({
9
+ selectedCount: 3,
10
+ onapprove: vi.fn(),
11
+ onreject: vi.fn(),
12
+ ondelete: vi.fn(),
13
+ onexport: vi.fn(),
14
+ onclear: vi.fn(),
15
+ ...over,
16
+ });
17
+ describe('BulkActions', () => {
18
+ it('shows the selection count and bulk action buttons', () => {
19
+ render(BulkActions, { props: baseProps() });
20
+ expect(screen.getByText('3')).toBeInTheDocument();
21
+ expect(screen.getByText('items selected')).toBeInTheDocument();
22
+ expect(screen.getByRole('button', { name: 'Approve All' })).toBeInTheDocument();
23
+ });
24
+ it('invokes onapprove when Approve All is clicked', async () => {
25
+ const onapprove = vi.fn();
26
+ render(BulkActions, { props: baseProps({ onapprove }) });
27
+ await userEvent.click(screen.getByRole('button', { name: 'Approve All' }));
28
+ expect(onapprove).toHaveBeenCalledTimes(1);
29
+ });
30
+ it('clears the selection', async () => {
31
+ const onclear = vi.fn();
32
+ render(BulkActions, { props: baseProps({ onclear }) });
33
+ await userEvent.click(screen.getByRole('button', { name: 'Clear' }));
34
+ expect(onclear).toHaveBeenCalledTimes(1);
35
+ });
36
+ it('renders nothing when no items are selected', () => {
37
+ const { container } = render(BulkActions, {
38
+ props: baseProps({ selectedCount: 0 }),
39
+ });
40
+ expect(container.querySelector('.bulk-actions')).toBeNull();
41
+ });
42
+ it('is axe-clean', async () => {
43
+ const { container } = render(BulkActions, { props: baseProps() });
44
+ await expectNoA11yViolations(container);
45
+ });
46
+ });
@@ -0,0 +1,23 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * First component test in smrt-projects via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it } from 'vitest';
7
+ import DurationDisplay from '../DurationDisplay.svelte';
8
+ describe('DurationDisplay', () => {
9
+ it('renders decimal hours to one decimal place', () => {
10
+ render(DurationDisplay, { props: { hours: 2.5 } });
11
+ expect(screen.getByText('2.5')).toBeInTheDocument();
12
+ });
13
+ it('renders an HH:MM value in hhmm format', () => {
14
+ render(DurationDisplay, { props: { hours: 1.5, format: 'hhmm' } });
15
+ expect(screen.getByText('1:30')).toBeInTheDocument();
16
+ });
17
+ it('is axe-clean', async () => {
18
+ const { container } = render(DurationDisplay, {
19
+ props: { hours: 8, showLabel: true },
20
+ });
21
+ await expectNoA11yViolations(container);
22
+ });
23
+ });