@per-diem-calculator/vanilla 1.0.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 (103) hide show
  1. package/.prettierrc +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1 -0
  4. package/eslint.config.js +29 -0
  5. package/index.html +11 -0
  6. package/package.json +49 -0
  7. package/public/output.css +2503 -0
  8. package/src/css/_styles.css +8 -0
  9. package/src/css/colors.css +45 -0
  10. package/src/css/fonts.css +9 -0
  11. package/src/css/rows/_heights.css +6 -0
  12. package/src/css/rows/_index.css +15 -0
  13. package/src/css/rows/add.css +18 -0
  14. package/src/css/rows/animate-btns.css +18 -0
  15. package/src/css/rows/animate-row-close.css +18 -0
  16. package/src/css/rows/animate-row-open.css +14 -0
  17. package/src/css/rows/animate-row-other.css +5 -0
  18. package/src/css/rows/btn-add-row.css +41 -0
  19. package/src/css/rows/btn-delete.css +22 -0
  20. package/src/css/rows/btn-expenses-calculate.css +22 -0
  21. package/src/css/rows/btn-expenses-category.css +22 -0
  22. package/src/css/rows/delete.css +10 -0
  23. package/src/css/rows/details.css +22 -0
  24. package/src/css/rows/expense.css +18 -0
  25. package/src/css/rows/location.css +34 -0
  26. package/src/css/rows/summary.css +22 -0
  27. package/src/css/tom-select/defaults.css +530 -0
  28. package/src/css/tom-select/overrides.css +55 -0
  29. package/src/css/tw-shadow-props.css +50 -0
  30. package/src/index.ts +1 -0
  31. package/src/ts/components/Button/Button.ts +50 -0
  32. package/src/ts/components/Button/template.html +34 -0
  33. package/src/ts/components/ExpenseRow/ExpenseRow.ts +397 -0
  34. package/src/ts/components/ExpenseRow/template.html +260 -0
  35. package/src/ts/components/Label/Label.ts +45 -0
  36. package/src/ts/components/Label/template.html +1 -0
  37. package/src/ts/components/LocationCategory/LocationCategory.ts +226 -0
  38. package/src/ts/components/LocationCategory/template.html +520 -0
  39. package/src/ts/components/LocationDate/LocationDate.ts +366 -0
  40. package/src/ts/components/LocationDate/template.html +27 -0
  41. package/src/ts/components/LocationSelect/LocationSelect.ts +299 -0
  42. package/src/ts/components/LocationSelect/template.html +45 -0
  43. package/src/ts/components/index.ts +6 -0
  44. package/src/ts/controller.ts +193 -0
  45. package/src/ts/model.ts +163 -0
  46. package/src/ts/types/config.ts +22 -0
  47. package/src/ts/types/dates.ts +82 -0
  48. package/src/ts/types/expenses.ts +73 -0
  49. package/src/ts/types/locations.ts +25 -0
  50. package/src/ts/utils/config/configDefault.ts +13 -0
  51. package/src/ts/utils/config/index.ts +12 -0
  52. package/src/ts/utils/config/numbers.ts +24 -0
  53. package/src/ts/utils/config/sanitizeConfig.ts +39 -0
  54. package/src/ts/utils/dates/INPUT_DATE_MINMAX.ts +5 -0
  55. package/src/ts/utils/dates/YEAR_REGEX.ts +4 -0
  56. package/src/ts/utils/dates/getDateSlice.ts +54 -0
  57. package/src/ts/utils/dates/getValidAPIYear.ts +17 -0
  58. package/src/ts/utils/dates/index.ts +19 -0
  59. package/src/ts/utils/dates/isDateRaw.ts +90 -0
  60. package/src/ts/utils/dates/isShortMonth.ts +24 -0
  61. package/src/ts/utils/dates/isYYYY.ts +10 -0
  62. package/src/ts/utils/dates/offsetDateString.ts +17 -0
  63. package/src/ts/utils/expenses/INTL_MIE_RATES.ts +2125 -0
  64. package/src/ts/utils/expenses/createExpenseObjs.ts +35 -0
  65. package/src/ts/utils/expenses/getLodgingRateDomestic.ts +73 -0
  66. package/src/ts/utils/expenses/getLodgingRateIntl.ts +119 -0
  67. package/src/ts/utils/expenses/getMieRates.ts +84 -0
  68. package/src/ts/utils/expenses/index.ts +5 -0
  69. package/src/ts/utils/expenses/parseIntlLodgingRates.ts +124 -0
  70. package/src/ts/utils/expenses/returnValidStateExpense.ts +46 -0
  71. package/src/ts/utils/fetch/fetchJsonGSA.ts +29 -0
  72. package/src/ts/utils/fetch/fetchXmlDOD.ts +38 -0
  73. package/src/ts/utils/fetch/index.ts +3 -0
  74. package/src/ts/utils/fetch/memoize.ts +46 -0
  75. package/src/ts/utils/fetch/parseXml.ts +19 -0
  76. package/src/ts/utils/locations/getCitiesDomestic.ts +48 -0
  77. package/src/ts/utils/locations/getCitiesIntl.ts +63 -0
  78. package/src/ts/utils/locations/getCountriesDomestic.ts +237 -0
  79. package/src/ts/utils/locations/getCountriesIntl.ts +34 -0
  80. package/src/ts/utils/locations/index.ts +6 -0
  81. package/src/ts/utils/locations/keepUniqueLocations.ts +12 -0
  82. package/src/ts/utils/locations/locationKeys.ts +10 -0
  83. package/src/ts/utils/locations/returnValidStateLocation.ts +13 -0
  84. package/src/ts/utils/locations/sortLocations.ts +19 -0
  85. package/src/ts/utils/misc/USD.ts +4 -0
  86. package/src/ts/utils/misc/debounce.ts +22 -0
  87. package/src/ts/utils/misc/handlePointerDown.ts +3 -0
  88. package/src/ts/utils/misc/handlePointerUp.ts +22 -0
  89. package/src/ts/utils/misc/inPrimitiveType.ts +4 -0
  90. package/src/ts/utils/misc/index.ts +6 -0
  91. package/src/ts/utils/misc/wait.ts +4 -0
  92. package/src/ts/utils/styles/applyStyles.ts +19 -0
  93. package/src/ts/utils/styles/highlightInput.ts +15 -0
  94. package/src/ts/utils/styles/index.ts +3 -0
  95. package/src/ts/utils/styles/removeStyles.ts +14 -0
  96. package/src/ts/views/Expense/Expense.ts +465 -0
  97. package/src/ts/views/Expense/template.html +176 -0
  98. package/src/ts/views/Location/Location.ts +763 -0
  99. package/src/ts/views/Location/template-row.html +146 -0
  100. package/src/ts/views/Location/template.html +130 -0
  101. package/src/ts/views/index.ts +2 -0
  102. package/tsconfig.json +27 -0
  103. package/vite.config.ts +12 -0
@@ -0,0 +1,397 @@
1
+ // Types
2
+ import type { StateExpenseItemValid } from '../../types/expenses';
3
+
4
+ // Utils
5
+ import {
6
+ handlePointerDown,
7
+ handlePointerUp,
8
+ USD,
9
+ wait,
10
+ } from '../../utils/misc';
11
+ import {
12
+ applyStyles,
13
+ removeStyles,
14
+ highlightSuccess,
15
+ } from '../../utils/styles';
16
+ import { ROW_CLOSED_HEIGHT, ROW_ANIMATE_MS } from '../../utils/config';
17
+ import { getShortMonth, getDD, getYYYY, getMM } from '../../utils/dates';
18
+
19
+ // HTML/CSS
20
+ import templateHTML from './template.html?raw';
21
+
22
+ // Template for this Custom Element
23
+ const template = document.createElement('template');
24
+
25
+ // Custom Element
26
+ export class PdcExpenseRow extends HTMLElement {
27
+ /* SETUP
28
+ */
29
+ #expense: StateExpenseItemValid;
30
+ #expensesCategory;
31
+ #styled: boolean;
32
+ #maxLodging: number;
33
+ #maxMie: number;
34
+ #lodgingAmount: number;
35
+ #mieAmount: number;
36
+
37
+ constructor(
38
+ expense: StateExpenseItemValid,
39
+ styled: boolean,
40
+ expensesCategory: 'mie' | 'lodging' | 'both',
41
+ ) {
42
+ super();
43
+ this.attachShadow({ mode: 'open' });
44
+
45
+ this.#expense = expense;
46
+ this.#styled = styled;
47
+ this.#expensesCategory = expensesCategory;
48
+
49
+ const { deductions, rates, lodgingAmount, mieAmount } = this.#expense;
50
+ this.#maxLodging = rates.maxLodging;
51
+ this.#maxMie =
52
+ deductions.FirstLastDay ? rates.maxMieFirstLast : rates.maxMie;
53
+ this.#lodgingAmount = lodgingAmount;
54
+ this.#mieAmount = mieAmount;
55
+
56
+ this.render(styled);
57
+ }
58
+
59
+ render(styled: boolean) {
60
+ this.#shadowRoot.innerHTML = '';
61
+ if (styled) {
62
+ template.innerHTML = templateHTML;
63
+ applyStyles(this.#shadowRoot);
64
+ } else template.innerHTML = removeStyles(templateHTML);
65
+ this.#shadowRoot.appendChild(template.content.cloneNode(true));
66
+
67
+ // Update text elements w/ values
68
+ this.#setRowDateText();
69
+ this.#setRowDetailsText();
70
+
71
+ // Update custom element's attribute values
72
+ this.setAttribute('date', this.#expense.date);
73
+ this.#updateLodgingAmount(this.#lodgingAmount.toString());
74
+ this.updateMieAmount(this.#mieAmount);
75
+ this.#updateTotalAmount();
76
+
77
+ this.#disableUnusedRowEls();
78
+ this.#addEventListeners();
79
+ }
80
+
81
+ #setRowDateText() {
82
+ const monthEl = this.#shadowRoot.querySelector('#month');
83
+ const dayEl = this.#shadowRoot.querySelector('#day');
84
+ const yearEl = this.#shadowRoot.querySelector('#year');
85
+ const date = new Date(this.#expense.date);
86
+ if (!(monthEl && dayEl && yearEl))
87
+ throw new Error(`Failed to render row's date elements.`);
88
+ monthEl.textContent = getShortMonth(date.toUTCString());
89
+ dayEl.textContent = getDD(this.#expense.date);
90
+ yearEl.textContent = getYYYY(this.#expense.date);
91
+ }
92
+
93
+ #setRowDetailsText() {
94
+ const locationEl = this.#shadowRoot.querySelector('#location');
95
+ const lodgingRateEl = this.#shadowRoot.querySelector('#lodging-rate');
96
+ const mieRateEl = this.#shadowRoot.querySelector('#mie-rate');
97
+ if (!(locationEl && lodgingRateEl && mieRateEl))
98
+ throw new Error(`Failed to render row's rate elements.`);
99
+
100
+ locationEl.textContent = `${this.#expense.city} (${this.#expense.country})`;
101
+ lodgingRateEl.setAttribute(
102
+ 'text',
103
+ `<p class="font-semibold">Lodging</p>
104
+ <p class="text-sm sm:text-base">Max ${USD.format(this.#maxLodging)}</p>`,
105
+ );
106
+ mieRateEl.setAttribute(
107
+ 'text',
108
+ `<p class="font-semibold">M&IE</p>
109
+ <p class="text-sm sm:text-base">Max ${USD.format(this.#maxMie)}</p>`,
110
+ );
111
+ }
112
+
113
+ #disableUnusedRowEls() {
114
+ const lodgingEl = this.#shadowRoot.querySelector('#lodging');
115
+ const deductionsEl = this.#shadowRoot.querySelector('#deductions');
116
+ if (this.#expensesCategory === 'mie') {
117
+ lodgingEl?.classList.add('disabled');
118
+ this.#shadowRoot
119
+ .querySelector<HTMLInputElement>('#lodging-amount')
120
+ ?.setAttribute('disabled', '');
121
+ }
122
+ if (this.#expensesCategory === 'lodging') {
123
+ deductionsEl?.classList.add('disabled');
124
+ deductionsEl
125
+ ?.querySelectorAll('input')
126
+ .forEach(el => el.setAttribute('disabled', ''));
127
+ }
128
+ }
129
+
130
+ /* EVENTS
131
+ */
132
+ #addEventListeners() {
133
+ // Input change events
134
+ this.#row.addEventListener('change', e => {
135
+ this.#handleInputs(e);
136
+ });
137
+
138
+ // Mouse, touch events
139
+ let pointerStartX = 0;
140
+ let pointerStartY = 0;
141
+ this.#row.addEventListener('pointerdown', e => {
142
+ if (!(e instanceof PointerEvent)) return;
143
+ const result = handlePointerDown(e);
144
+ pointerStartX = result.pointerStartX;
145
+ pointerStartY = result.pointerStartY;
146
+ });
147
+ this.#row.addEventListener('pointerup', e => {
148
+ if (e instanceof PointerEvent) {
149
+ const result = handlePointerUp(
150
+ e,
151
+ this.#handleClicks.bind(this),
152
+ pointerStartX,
153
+ pointerStartY,
154
+ );
155
+ pointerStartX = result.pointerStartX;
156
+ pointerStartY = result.pointerStartY;
157
+ }
158
+ });
159
+
160
+ // Keyboard events
161
+ this.#row.addEventListener('keydown', e => {
162
+ if (!(e.key === 'Enter' || e.key === ' ')) return;
163
+ this.#handleClicks(e);
164
+ });
165
+ }
166
+
167
+ #handleInputs(e: Event) {
168
+ const target = e.target;
169
+ if (!(target instanceof HTMLInputElement)) return;
170
+ if (target.getAttribute('id') === 'lodging-amount') {
171
+ this.#updateLodgingAmount(target.value);
172
+ return;
173
+ }
174
+ const attrName = target.getAttribute('name');
175
+ if (
176
+ !(
177
+ attrName === 'breakfast' ||
178
+ attrName === 'lunch' ||
179
+ attrName === 'dinner'
180
+ )
181
+ )
182
+ return;
183
+
184
+ this.setAttribute(`${attrName}provided`, target.checked ? 'yes' : 'no');
185
+ }
186
+
187
+ #handleClicks(e: Event) {
188
+ const target = e.target;
189
+ if (!(target instanceof SVGElement || target instanceof HTMLElement))
190
+ return;
191
+ if (target.closest('[data-pdc="expense-row-toggle"]')) this.rowToggle();
192
+ if (
193
+ !!target.closest('#deductions') &&
194
+ e instanceof KeyboardEvent &&
195
+ target instanceof HTMLLabelElement
196
+ ) {
197
+ target.click();
198
+ }
199
+ }
200
+
201
+ /* GET ELS
202
+ */
203
+ get #row() {
204
+ const el = this.#shadowRoot.querySelector<HTMLElement>('#expense-row');
205
+ if (!el) throw new Error('Failed to render row elements.');
206
+ return el;
207
+ }
208
+
209
+ get #rowAnimatedEls() {
210
+ const summary = this.#row.querySelector<HTMLElement>(
211
+ '[data-pdc="expense-row-summary"]',
212
+ );
213
+ const details = this.#row.querySelector<HTMLElement>(
214
+ '[data-pdc="expense-row-details"]',
215
+ );
216
+ if (!(summary && details))
217
+ throw new Error('Failed to render row summary elements.');
218
+ return {
219
+ summary,
220
+ details,
221
+ };
222
+ }
223
+
224
+ get #shadowRoot() {
225
+ if (!this.shadowRoot)
226
+ throw new Error(
227
+ 'Failed to create ShadowRoot for expense row custom element',
228
+ );
229
+ return this.shadowRoot;
230
+ }
231
+
232
+ /* VISUAL METHODS
233
+ */
234
+ rowToggle = async (toggle: 'open' | 'close' | null = null) => {
235
+ if (!this.#styled || this.#row.classList.contains('toggling')) return;
236
+
237
+ if (!toggle) {
238
+ this.rowToggle(
239
+ this.#row.offsetHeight === ROW_CLOSED_HEIGHT ? 'open' : 'close',
240
+ );
241
+ return;
242
+ }
243
+
244
+ this.#row.classList.remove('pdc-row-open', 'pdc-row-close');
245
+ this.#row.classList.add('toggling', `pdc-row-${toggle}`);
246
+
247
+ await this.#animateRow(toggle);
248
+ this.#row.classList.remove('toggling');
249
+ };
250
+
251
+ #animateRow = async (direction: 'open' | 'close') => {
252
+ await wait(ROW_ANIMATE_MS);
253
+ this.#enableRowTabIndex(direction === 'open' ? true : false);
254
+ this.#row.style.height = `${direction === 'open' ? this.#row.scrollHeight : this.#row.clientHeight}px`;
255
+ this.#rowAnimatedEls.summary.style.opacity =
256
+ direction === 'open' ? '0' : '100';
257
+ this.#rowAnimatedEls.summary.style.transform =
258
+ direction === 'open' ? `translateY(-200%)` : `translateY(0%)`;
259
+ this.#rowAnimatedEls.details.style.opacity =
260
+ direction === 'open' ? '100' : '0';
261
+ this.#rowAnimatedEls.details.style.transform =
262
+ direction === 'open' ? `translateX(100%)` : `translateX(0%)`;
263
+ };
264
+
265
+ windowResize = () => {
266
+ if (this.#row.offsetHeight === ROW_CLOSED_HEIGHT) return;
267
+ this.#row.style.height =
268
+ this.#rowAnimatedEls.details.scrollHeight + 'px';
269
+ };
270
+
271
+ styleRow = () => {
272
+ if (!this.parentNode)
273
+ throw new Error(`Failed to get row index in Expense view.`);
274
+ const index = Array.from(
275
+ this.parentNode.querySelectorAll('pdc-expense-row'),
276
+ ).indexOf(this);
277
+ const color = index % 2 === 0 ? 'neutral-50' : 'white';
278
+ const oppColor = color === 'neutral-50' ? 'white' : 'neutral-50';
279
+ this.#row.classList.remove('bg-neutral-50', 'bg-white');
280
+ this.#row.classList.add(`bg-${color}`);
281
+ [...this.#rowAnimatedEls.details.children].forEach((el, i) => {
282
+ el.classList.remove('bg-white', 'bg-neutral-50');
283
+ el.classList.add(i % 2 === 0 ? `bg-${color}` : `bg-${oppColor}`);
284
+ });
285
+ this.style.position = 'relative';
286
+ this.style.overflow = 'hidden';
287
+ this.style.zIndex = index.toString();
288
+ };
289
+
290
+ /* UPDATE METHODS
291
+ */
292
+
293
+ #enableRowTabIndex(enable: boolean) {
294
+ this.#shadowRoot
295
+ .querySelector('[data-pdc="expense-row-details"]')
296
+ ?.querySelectorAll('[tabindex]')
297
+ .forEach(el => {
298
+ el.setAttribute('tabindex', enable ? '0' : '-1');
299
+ });
300
+ }
301
+
302
+ updateMieAmount(amount: number) {
303
+ const mieTotalAmountInp =
304
+ this.#shadowRoot.querySelector<HTMLInputElement>(
305
+ '#mie-total-amount',
306
+ );
307
+ const mieSummary = this.#shadowRoot.querySelector<HTMLInputElement>(
308
+ '#summary-mie-amount',
309
+ );
310
+ if (!(mieTotalAmountInp && mieSummary))
311
+ throw new Error(`Failed to render row's M&IE elements.`);
312
+ const oldAmount = mieTotalAmountInp.value;
313
+ const newAmount = amount.toFixed(2).toString();
314
+ if (oldAmount === newAmount) return;
315
+
316
+ mieTotalAmountInp.value = newAmount;
317
+ mieSummary.textContent = USD.format(amount);
318
+ this.#mieAmount = amount;
319
+ if (this.#styled) highlightSuccess(mieTotalAmountInp);
320
+ this.#updateTotalAmount();
321
+ }
322
+
323
+ #updateLodgingAmount(value: string) {
324
+ const lodgingInput =
325
+ this.#shadowRoot.querySelector<HTMLInputElement>('#lodging-amount');
326
+ const lodgingSummary = this.#shadowRoot.querySelector<HTMLInputElement>(
327
+ '#summary-lodging-amount',
328
+ );
329
+ if (!(lodgingInput && lodgingSummary))
330
+ throw new Error(
331
+ 'Failed to render input element for lodging in Expense row.',
332
+ );
333
+
334
+ // Check if input was a valid amount. If yes, adopt it for the row. If no, reset the row's lodging amount to match the max lodging rate.
335
+
336
+ const isValidLodgingAmount = (value: string) => {
337
+ return (
338
+ !isNaN(parseFloat(value)) &&
339
+ +value >= 0 &&
340
+ +value <= this.#maxLodging
341
+ );
342
+ };
343
+
344
+ if (isValidLodgingAmount(value)) {
345
+ this.setAttribute('lodging', value);
346
+ lodgingInput.value = (+value).toFixed(2).toString();
347
+ this.#lodgingAmount = +value;
348
+ lodgingSummary.textContent = USD.format(+value);
349
+ } else {
350
+ this.setAttribute('lodging', this.#maxLodging.toString());
351
+ lodgingInput.value = this.#maxLodging.toFixed(2).toString();
352
+ this.#lodgingAmount = this.#maxLodging;
353
+ lodgingSummary.textContent = USD.format(this.#maxLodging);
354
+ }
355
+
356
+ if (this.#styled) highlightSuccess(lodgingInput);
357
+ this.#updateTotalAmount();
358
+ }
359
+
360
+ #updateTotalAmount() {
361
+ const totalAmount = this.#lodgingAmount + this.#mieAmount;
362
+ const totalEls = this.#shadowRoot.querySelectorAll(
363
+ '[data-pdc="expense-total"]',
364
+ );
365
+ totalEls?.forEach(el => {
366
+ if (el instanceof HTMLInputElement) {
367
+ el.value = totalAmount.toFixed(2).toString();
368
+ if (this.#styled) highlightSuccess(el);
369
+ }
370
+ if (el instanceof HTMLParagraphElement)
371
+ el.textContent = `${USD.format(totalAmount)}`;
372
+ });
373
+ }
374
+
375
+ /* GET DATA METHODS
376
+ */
377
+ get rateSource() {
378
+ return this.#expense.source;
379
+ }
380
+
381
+ get rateString() {
382
+ const { effDate, ...rates } = this.#expense.rates;
383
+ const { country, city } = this.#expense;
384
+ return JSON.stringify({ city, country, rates });
385
+ }
386
+
387
+ get rateStringForTable() {
388
+ const { date, country, city, rates } = this.#expense;
389
+ const monthYear = `${getMM(date)}/${getYYYY(date)}`;
390
+ return JSON.stringify({ monthYear, country, city, rates });
391
+ }
392
+
393
+ get amount() {
394
+ const { mieAmount: mie, lodgingAmount: lodging } = this.#expense;
395
+ return { mie, lodging };
396
+ }
397
+ }
@@ -0,0 +1,260 @@
1
+ <div
2
+ id="expense-row"
3
+ class="group [.pdc-row-open]:animate-row-expense-open
4
+ [.pdc-row-close]:animate-row-expense-close relative flex h-24
5
+ items-start overflow-hidden bg-white ring-2 ring-neutral-200
6
+ transition-shadow duration-500"
7
+ >
8
+ <!-- Row sidebar: date, toggle -->
9
+ <div
10
+ data-pdc="expense-row-sidebar"
11
+ class="relative z-10 mr-4 ml-2 h-[var(--row-expense-open)] sm:mr-6
12
+ sm:ml-4"
13
+ >
14
+ <div
15
+ data-pdc="expense-row-toggle"
16
+ class="flex h-24 w-full items-center text-neutral-800
17
+ hover:cursor-pointer *:hover:cursor-pointer
18
+ group-not-[.pdc-row-close]:hover:[&_button_svg]:!-rotate-0
19
+ group-[.pdc-row-close]:hover:[&_button_svg]:!-rotate-180"
20
+ >
21
+ <button
22
+ title="Toggle row"
23
+ tabindex="0"
24
+ type="button"
25
+ class="focus-visible:border-primary-800 rounded-full border-3
26
+ border-transparent transition-colors focus:outline-none"
27
+ >
28
+ <svg
29
+ class="size-5 shrink-0 fill-none stroke-current
30
+ transition-transform duration-500
31
+ group-[.pdc-row-close]:rotate-0
32
+ group-[.pdc-row-open]:-rotate-180 sm:size-6"
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ viewBox="0 0 24 24"
35
+ >
36
+ <path
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ stroke-width="2"
40
+ d="M19 9l-7 7-7-7"
41
+ />
42
+ </svg>
43
+ </button>
44
+ <div
45
+ class="ml-2 flex flex-col text-center text-xs font-semibold
46
+ uppercase sm:text-sm"
47
+ >
48
+ <span id="month"></span>
49
+ <span id="day" class="text-xl"></span>
50
+ <span id="year"></span>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Row contents: summary, details -->
56
+ <div
57
+ class="relative z-10 flex h-[var(--row-expense-open)] w-full items-start
58
+ overflow-hidden"
59
+ >
60
+ <!-- Summary -->
61
+ <div
62
+ data-pdc="expense-row-toggle"
63
+ class="ml-1 flex h-22 w-full items-center justify-between"
64
+ >
65
+ <div
66
+ data-pdc="expense-row-summary"
67
+ inert
68
+ class="group-[.pdc-row-open]:animate-row-open-summary
69
+ group-[.pdc-row-close]:animate-row-close-summary m ml-[30%]
70
+ flex w-full max-w-[50lvh] flex-col justify-between gap-y-0.5
71
+ overflow-hidden pr-4 text-sm
72
+ group-[.pdc-row-add]:translate-y-[-200%]
73
+ group-[.pdc-row-add]:opacity-0 sm:ml-[10%] sm:flex
74
+ sm:max-w-full sm:flex-row sm:justify-between sm:pr-12
75
+ sm:text-base"
76
+ >
77
+ <div
78
+ class="grid grid-cols-2 sm:flex sm:flex-col"
79
+ id="summary-mie"
80
+ >
81
+ <p>M&IE</p>
82
+ <p class="text-right sm:text-left" id="summary-mie-amount">
83
+ &nbsp;
84
+ </p>
85
+ </div>
86
+ <div
87
+ class="grid grid-cols-2 sm:flex sm:flex-col"
88
+ id="summary-lodging"
89
+ >
90
+ <p>Lodging</p>
91
+ <p
92
+ class="text-right sm:text-left"
93
+ id="summary-lodging-amount"
94
+ >
95
+ &nbsp;
96
+ </p>
97
+ </div>
98
+ <div
99
+ class="grid grid-cols-2 sm:flex sm:flex-col"
100
+ id="summary-total"
101
+ >
102
+ <p>Total</p>
103
+ <p class="text-right sm:text-left" data-pdc="expense-total">
104
+ &nbsp;
105
+ </p>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Details -->
111
+ <div
112
+ data-pdc="expense-row-details"
113
+ class="group-[.pdc-row-open]:animate-row-open-details
114
+ group-[.pdc-row-add]:animate-row-open-details
115
+ group-[.pdc-row-close]:animate-row-close-details absolute top-0
116
+ left-[-100%] w-full overflow-hidden border-l-2
117
+ border-l-neutral-200 transition-colors"
118
+ >
119
+ <!-- Location -->
120
+ <div
121
+ class="flex items-center justify-between border-b
122
+ border-b-neutral-100 px-3 py-2"
123
+ >
124
+ <p
125
+ id="location"
126
+ class="relative my-3 truncate text-neutral-800"
127
+ ></p>
128
+ </div>
129
+
130
+ <!-- Deductions -->
131
+ <div
132
+ id="deductions"
133
+ class="items-center justify-between border-b
134
+ border-b-neutral-100 px-3 py-4
135
+ [.disabled]:cursor-not-allowed [.disabled]:**:opacity-30"
136
+ >
137
+ <pdc-label styled="true" text="I was provided"></pdc-label>
138
+ <fieldset class="my-3 space-y-3">
139
+ <div
140
+ class="*:has-checked:bg-primary-50 *:hover:bg-primary-50
141
+ *:has-checked:text-primary-800
142
+ *:hover:text-primary-800
143
+ *:focus-visible:!bg-primary-50
144
+ *:focus-visible:!text-primary-800
145
+ *:focus-visible:ring-primary-800 grid grid-cols-3
146
+ divide-x overflow-hidden rounded-lg border
147
+ bg-transparent text-center text-neutral-700
148
+ *:cursor-pointer *:px-4 *:py-2.5 *:text-sm
149
+ *:font-medium *:ring-2 *:ring-transparent
150
+ *:transition-[box-shadow_background-color_color]
151
+ *:duration-500 *:select-none *:ring-inset
152
+ *:focus:outline-none *:focus-visible:ring-6"
153
+ >
154
+ <label tabindex="-1" ="breakfast"
155
+ >Break<span class="sm:hidden" aria-hidden="true"
156
+ >.</span
157
+ ><span class="hidden sm:inline-block">fast</span
158
+ ><input
159
+ type="checkbox"
160
+ name="breakfast"
161
+ id="breakfast"
162
+ class="hidden"
163
+ /></label>
164
+ <label tabindex="-1" for="lunch" class="peer"
165
+ >Lunch<input
166
+ type="checkbox"
167
+ name="lunch"
168
+ id="lunch"
169
+ class="hidden"
170
+ /></label>
171
+ <label tabindex="-1" for="dinner" class="peer"
172
+ >Dinner<input
173
+ type="checkbox"
174
+ name="dinner"
175
+ id="dinner"
176
+ class="hidden"
177
+ /></label>
178
+ </div>
179
+ </fieldset>
180
+ <!-- M&IE -->
181
+ <div id="mie" class="flex items-center justify-between">
182
+ <pdc-label styled="true" id="mie-rate"></pdc-label>
183
+
184
+ <input
185
+ id="mie-total-amount"
186
+ type="number"
187
+ inputmode="decimal"
188
+ step="0.01"
189
+ min="0"
190
+ disabled
191
+ class="[.success]:border-r-success-400 relative my-3
192
+ w-[50%] [appearance:textfield] border-r-3
193
+ border-r-transparent bg-transparent py-2 pr-3
194
+ text-right text-neutral-800
195
+ transition-[border-color]
196
+ disabled:cursor-not-allowed
197
+ [&::-webkit-inner-spin-button]:appearance-none
198
+ [&::-webkit-outer-spin-button]:appearance-none"
199
+ />
200
+ </div>
201
+ </div>
202
+
203
+ <!-- Lodging -->
204
+ <div
205
+ id="lodging"
206
+ class="flex items-center justify-between border-b
207
+ border-b-neutral-100 bg-white px-3 py-2
208
+ [.disabled]:cursor-not-allowed [.disabled]:**:opacity-30"
209
+ >
210
+ <pdc-label styled="true" id="lodging-rate"></pdc-label>
211
+
212
+ <input
213
+ tabindex="-1"
214
+ id="lodging-amount"
215
+ type="number"
216
+ inputmode="decimal"
217
+ step="0.01"
218
+ min="0"
219
+ class="[.success]:border-r-success-400
220
+ selection:bg-primary-800 relative my-3 w-[50%]
221
+ [appearance:textfield] border-r-3 border-b-2
222
+ border-r-transparent border-b-neutral-200 bg-transparent
223
+ py-2 pr-3 text-right text-neutral-800
224
+ transition-[border-color] selection:text-neutral-50
225
+ focus:outline-none disabled:cursor-not-allowed
226
+ disabled:border-b-transparent
227
+ [&::-webkit-inner-spin-button]:appearance-none
228
+ [&::-webkit-outer-spin-button]:appearance-none"
229
+ />
230
+ </div>
231
+
232
+ <!-- Total -->
233
+ <div
234
+ id="total"
235
+ class="group flex items-center justify-between border-b
236
+ border-b-neutral-100 bg-white px-3"
237
+ >
238
+ <pdc-label
239
+ styled="true"
240
+ text="<p class='font-semibold'>Total</p>"
241
+ ></pdc-label>
242
+
243
+ <input
244
+ data-pdc="expense-total"
245
+ type="number"
246
+ inputmode="decimal"
247
+ step="0.01"
248
+ min="0"
249
+ disabled
250
+ class="[.success]:border-r-success-400 relative my-3 w-[50%]
251
+ [appearance:textfield] border-r-3 border-r-transparent
252
+ bg-transparent py-2 pr-3 text-right text-neutral-800
253
+ transition-[border-color] disabled:cursor-not-allowed
254
+ [&::-webkit-inner-spin-button]:appearance-none
255
+ [&::-webkit-outer-spin-button]:appearance-none"
256
+ />
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>