@ossy/timesheets 3.7.0 → 3.8.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ossy/timesheets",
3
3
  "description": "Timesheets — generate, review, save, and export monthly work hours",
4
- "version": "3.7.0",
4
+ "version": "3.8.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "module": "./src/index.js",
@@ -14,7 +14,7 @@
14
14
  "src": "./src"
15
15
  },
16
16
  "dependencies": {
17
- "@ossy/resources": "^3.7.0",
17
+ "@ossy/resources": "^3.8.0",
18
18
  "html-to-image": "^1.11.13",
19
19
  "nanoid": "^5.1.11",
20
20
  "pdf-lib": "^1.17.1"
@@ -38,5 +38,5 @@
38
38
  "/src",
39
39
  "README.md"
40
40
  ],
41
- "gitHead": "622d97535abad08415963405ace2f5aaaecd7ca4"
41
+ "gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
42
42
  }
@@ -0,0 +1,32 @@
1
+ import React, { useMemo } from 'react'
2
+ import { Select, Text, useLocale, View } from '@ossy/design-system'
3
+ import { HOLIDAY_COUNTRY_CODES } from '@ossy/calendar/holiday-locale'
4
+
5
+ export function HolidayLocaleSelect ({ value, onChange, disabled }) {
6
+ const { language, t } = useLocale()
7
+ const displayNames = useMemo(() => {
8
+ try {
9
+ return new Intl.DisplayNames([language || 'en'], { type: 'region' })
10
+ } catch {
11
+ return null
12
+ }
13
+ }, [language])
14
+
15
+ return (
16
+ <View gap="xs">
17
+ <Text variant="small" weight="medium" text="timesheets.holidayLocale" />
18
+ <Select
19
+ value={value}
20
+ onChange={onChange}
21
+ disabled={disabled}
22
+ aria-label={t('timesheets.holidayLocale')}
23
+ >
24
+ {HOLIDAY_COUNTRY_CODES.map((code) => (
25
+ <option key={code} value={code}>
26
+ {displayNames?.of(code) || code}
27
+ </option>
28
+ ))}
29
+ </Select>
30
+ </View>
31
+ )
32
+ }
package/src/Timesheet.jsx CHANGED
@@ -13,25 +13,39 @@ const calendarGrid = {
13
13
  }
14
14
 
15
15
  const dayCell = {
16
+ position: 'relative',
16
17
  boxSizing: 'border-box',
17
18
  display: 'flex',
18
- flexDirection: 'column',
19
19
  alignItems: 'center',
20
- justifyContent: 'flex-start',
21
- gap: 4,
20
+ justifyContent: 'center',
22
21
  width: '100%',
23
22
  minHeight: 72,
23
+ margin: 0,
24
24
  borderRadius: 'var(--space-xxs, 2px)',
25
- padding: '6px 4px',
25
+ padding: '18px 4px 8px',
26
+ cursor: 'text',
27
+ }
28
+
29
+ const dateNumber = {
30
+ position: 'absolute',
31
+ top: 4,
32
+ right: 5,
33
+ fontSize: 11,
34
+ fontWeight: 500,
35
+ lineHeight: 1,
36
+ letterSpacing: 0,
37
+ fontFamily: 'var(--text-default-font-family, sans-serif)',
38
+ pointerEvents: 'none',
26
39
  }
27
40
 
28
41
  const hoursInput = {
29
42
  width: '100%',
30
- maxWidth: 48,
31
- minHeight: 32,
32
- padding: '6px 4px',
43
+ minHeight: 28,
44
+ padding: 0,
33
45
  textAlign: 'center',
34
- fontSize: 13,
46
+ fontSize: 14,
47
+ fontWeight: 700,
48
+ lineHeight: 1,
35
49
  borderRadius: 'var(--space-xxs, 2px)',
36
50
  background: 'transparent',
37
51
  borderColor: 'transparent',
@@ -90,6 +104,11 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
90
104
  <>
91
105
  <style href="@ossy/timesheets/day-surface" precedence="high">
92
106
  {`
107
+ [data-timesheet-root] [data-timesheet-day-number] {
108
+ color: var(--foreground);
109
+ font-family: var(--text-default-font-family, sans-serif);
110
+ }
111
+
93
112
  [data-timesheet-root] [data-ossy-calendar-day] {
94
113
  background: var(--surface-primary);
95
114
  }
@@ -102,6 +121,31 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
102
121
  [data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"]:hover {
103
122
  background: color-mix(in srgb, hsl(350 32% 55%) 13%, var(--surface-primary));
104
123
  }
124
+
125
+ [data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"] [data-timesheet-day-number] {
126
+ color: color-mix(in srgb, hsl(350 28% 38%) 72%, var(--foreground));
127
+ }
128
+
129
+ [data-timesheet-root] [data-ossy-calendar-day]:focus-within {
130
+ background: color-mix(in srgb, var(--color-accent, #3dcf9a) 16%, var(--surface-primary));
131
+ box-shadow: inset 0 0 0 2px var(--color-accent, #3dcf9a);
132
+ }
133
+
134
+ [data-timesheet-root] [data-ossy-calendar-day][data-day-off="true"]:focus-within {
135
+ background: color-mix(in srgb, var(--color-accent, #3dcf9a) 14%, color-mix(in srgb, hsl(350 32% 58%) 9%, var(--surface-primary)));
136
+ }
137
+
138
+ [data-timesheet-root] [data-ossy-calendar-day] [data-component="input"] {
139
+ font-weight: 700;
140
+ font-size: 14px;
141
+ text-align: center;
142
+ }
143
+
144
+ [data-timesheet-root] [data-ossy-calendar-day] [data-component="input"]:focus {
145
+ outline: none;
146
+ border-color: transparent;
147
+ box-shadow: none;
148
+ }
105
149
  `}
106
150
  </style>
107
151
  <div
@@ -152,23 +196,20 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
152
196
  const off = !line.isWorkday
153
197
 
154
198
  return (
155
- <div
199
+ <label
156
200
  key={cell.dateKey}
157
201
  data-ossy-calendar-day
158
202
  data-day-off={off}
159
203
  style={dayCell}
160
204
  >
161
- <Text
205
+ <DateDisplay
206
+ value={line.date}
207
+ formatOptions={{ day: 'numeric' }}
162
208
  variant="small"
163
- style={{
164
- fontWeight: 600,
165
- fontSize: 13,
166
- lineHeight: 1.2,
167
- color: 'var(--foreground)',
168
- }}
169
- >
170
- {cell.day}
171
- </Text>
209
+ as="span"
210
+ data-timesheet-day-number
211
+ style={dateNumber}
212
+ />
172
213
  <Input
173
214
  type="number"
174
215
  min={0}
@@ -180,7 +221,7 @@ export function Timesheet ({ lines = [], onChange, disabled = false, periodStart
180
221
  style={hoursInput}
181
222
  aria-label="timesheets.hours"
182
223
  />
183
- </div>
224
+ </label>
184
225
  )
185
226
  })}
186
227
  </div>
@@ -6,6 +6,8 @@ import { TIMESHEET_EXPORT_FONT_FAMILY } from './export-capture.js'
6
6
  /** Matches on-screen Timesheet.jsx — solid colors for html-to-image capture. */
7
7
  const FG = 'hsl(199, 90%, 10%)'
8
8
  const FG_MUTED = 'hsl(182, 15%, 42%)'
9
+ const DATE_FG = 'hsl(182, 23%, 30%)'
10
+ const DATE_OFF_FG = 'hsl(350, 28%, 38%)'
9
11
  const ACCENT = 'hsl(167, 89%, 43%)'
10
12
  const CELL_BG = '#ffffff'
11
13
  const OFF_BG = 'color-mix(in srgb, hsl(350 32% 58%) 9%, #ffffff)'
@@ -22,16 +24,15 @@ const calendarGrid = {
22
24
  }
23
25
 
24
26
  const dayCell = {
27
+ position: 'relative',
25
28
  boxSizing: 'border-box',
26
29
  display: 'flex',
27
- flexDirection: 'column',
28
30
  alignItems: 'center',
29
- justifyContent: 'flex-start',
30
- gap: 4,
31
+ justifyContent: 'center',
31
32
  width: '100%',
32
33
  minHeight: 72,
33
34
  borderRadius: 2,
34
- padding: '6px 4px',
35
+ padding: '18px 4px 8px',
35
36
  background: CELL_BG,
36
37
  }
37
38
 
@@ -175,12 +176,14 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
175
176
  >
176
177
  <span
177
178
  style={{
178
- display: 'block',
179
+ position: 'absolute',
180
+ top: 4,
181
+ right: 5,
179
182
  fontFamily: TIMESHEET_EXPORT_FONT_FAMILY,
180
- fontSize: 13,
181
- fontWeight: 600,
182
- lineHeight: 1.2,
183
- color: 'inherit',
183
+ fontSize: 11,
184
+ fontWeight: 500,
185
+ lineHeight: 1,
186
+ color: off ? DATE_OFF_FG : DATE_FG,
184
187
  }}
185
188
  >
186
189
  {cell.day}
@@ -190,14 +193,11 @@ export function TimesheetExportCard ({ lines = [], periodStart, captureRef }) {
190
193
  display: 'block',
191
194
  boxSizing: 'border-box',
192
195
  width: '100%',
193
- maxWidth: 48,
194
- minHeight: 32,
195
- padding: '6px 4px',
196
196
  textAlign: 'center',
197
197
  fontFamily: TIMESHEET_EXPORT_FONT_FAMILY,
198
- fontSize: 13,
199
- fontWeight: 600,
200
- lineHeight: 1.2,
198
+ fontSize: 14,
199
+ fontWeight: 700,
200
+ lineHeight: 1,
201
201
  color: 'inherit',
202
202
  }}
203
203
  >
@@ -9,6 +9,8 @@ import { invokeErrorMessage } from './invoke-error-message.js'
9
9
  import { metadata as ListTimesheets } from './list.action.js'
10
10
  import { metadata as GenerateTimesheet } from './generate.action.js'
11
11
  import { metadata as OpenNewTimesheet } from './open-new.action.js'
12
+ import { HolidayLocaleSelect } from './HolidayLocaleSelect.jsx'
13
+ import { resolveHolidayLocale } from './holiday-locale.js'
12
14
 
13
15
  function buildMonthOptions (language, yearsBack = 1, yearsForward = 0) {
14
16
  let locale = 'en-GB'
@@ -46,6 +48,7 @@ export default function TimesheetsProductHome () {
46
48
  const n = new Date()
47
49
  return `${n.getFullYear()}-${n.getMonth()}`
48
50
  })
51
+ const [holidayLocale, setHolidayLocale] = useState(() => resolveHolidayLocale(language))
49
52
  const [creating, setCreating] = useState(false)
50
53
  const [sheets, setSheets] = useState([])
51
54
  const [loading, setLoading] = useState(true)
@@ -104,6 +107,7 @@ export default function TimesheetsProductHome () {
104
107
  setCreateError(null)
105
108
  const n = new Date()
106
109
  setPeriodKey(`${n.getFullYear()}-${n.getMonth()}`)
110
+ setHolidayLocale(resolveHolidayLocale(language))
107
111
  }
108
112
 
109
113
  const handleGenerate = async () => {
@@ -114,7 +118,7 @@ export default function TimesheetsProductHome () {
114
118
  const data = await sdk.invoke(GenerateTimesheet, {
115
119
  year: selected.year,
116
120
  monthIndex: selected.monthIndex,
117
- locale: language,
121
+ locale: holidayLocale,
118
122
  })
119
123
  if (data?.id) {
120
124
  openDetail(data.id)
@@ -196,6 +200,12 @@ export default function TimesheetsProductHome () {
196
200
  </Select>
197
201
  </View>
198
202
 
203
+ <HolidayLocaleSelect
204
+ value={holidayLocale}
205
+ disabled={generating}
206
+ onChange={(e) => setHolidayLocale(e.target.value)}
207
+ />
208
+
199
209
  {periodAlreadyExists && (
200
210
  <Text variant="small" color="secondary" text="timesheets.periodExistsHint" />
201
211
  )}
@@ -32,8 +32,9 @@
32
32
  "timesheets.detail.description": "Review and adjust hours, then save or export.",
33
33
  "timesheets.new": "New",
34
34
  "timesheets.createTitle": "New timesheet",
35
- "timesheets.createDescription": "Choose a month to generate a prefilled timesheet.",
35
+ "timesheets.createDescription": "Choose a month and holiday calendar to generate a prefilled timesheet.",
36
36
  "timesheets.period": "Period",
37
+ "timesheets.holidayLocale": "Holiday calendar",
37
38
  "timesheets.generate": "Generate",
38
39
  "timesheets.openExisting": "Open",
39
40
  "timesheets.periodExistsHint": "A timesheet already exists for this period — Open loads it.",
@@ -1,64 +1,6 @@
1
- /**
2
- * Map UI language / BCP 47 tags to ISO 3166-1 alpha-2 country codes
3
- * for `date-holidays` (CalendarClient.isWorkDay).
4
- *
5
- * Prefer an explicit region (`sv-SE` → `SE`, `en-US` → `US`).
6
- * Language-only tags fall back to a common country for that language.
7
- */
8
-
9
- /** @type {Record<string, string>} */
10
- const LANGUAGE_TO_COUNTRY = {
11
- sv: 'SE',
12
- en: 'GB',
13
- nb: 'NO',
14
- nn: 'NO',
15
- no: 'NO',
16
- da: 'DK',
17
- fi: 'FI',
18
- de: 'DE',
19
- fr: 'FR',
20
- nl: 'NL',
21
- es: 'ES',
22
- it: 'IT',
23
- pt: 'PT',
24
- pl: 'PL',
25
- }
26
-
27
- const DEFAULT_HOLIDAY_LOCALE = 'SE'
28
-
29
- /**
30
- * @param {unknown} value
31
- * @returns {string | null} Uppercase ISO country code, or null if invalid
32
- */
33
- export function normalizeHolidayLocale (value) {
34
- if (typeof value !== 'string') return null
35
- const trimmed = value.trim()
36
- if (!/^[A-Za-z]{2}$/.test(trimmed)) return null
37
- return trimmed.toUpperCase()
38
- }
39
-
40
- /**
41
- * @param {string | null | undefined} languageOrTag UI language or BCP 47 tag
42
- * @param {string} [fallback]
43
- * @returns {string} ISO country code for holiday calendars
44
- */
45
- export function resolveHolidayLocale (languageOrTag, fallback = DEFAULT_HOLIDAY_LOCALE) {
46
- if (typeof languageOrTag === 'string' && languageOrTag.trim()) {
47
- try {
48
- const loc = new Intl.Locale(languageOrTag.trim())
49
- const region = normalizeHolidayLocale(loc.region)
50
- if (region) return region
51
- const fromLanguage = LANGUAGE_TO_COUNTRY[loc.language?.toLowerCase()]
52
- if (fromLanguage) return fromLanguage
53
- } catch {
54
- // fall through
55
- }
56
-
57
- const asCountry = normalizeHolidayLocale(languageOrTag)
58
- if (asCountry) return asCountry
59
- }
60
-
61
- return normalizeHolidayLocale(fallback) || DEFAULT_HOLIDAY_LOCALE
62
- }
63
-
64
- export { DEFAULT_HOLIDAY_LOCALE }
1
+ export {
2
+ DEFAULT_HOLIDAY_LOCALE,
3
+ HOLIDAY_COUNTRY_CODES,
4
+ normalizeHolidayLocale,
5
+ resolveHolidayLocale,
6
+ } from '@ossy/calendar/holiday-locale'
package/src/index.js CHANGED
@@ -15,7 +15,7 @@ export { downloadTimesheetCsv } from './download-timesheet-csv.js'
15
15
  export { downloadTimesheetImage } from './download-timesheet-image.js'
16
16
  export { downloadTimesheetPdf } from './download-timesheet-pdf.js'
17
17
  export { buildPrefilledLines, monthPeriodBounds, WORKDAY_HOURS } from './build-prefilled-lines.js'
18
- export { resolveHolidayLocale, DEFAULT_HOLIDAY_LOCALE } from './holiday-locale.js'
18
+ export { resolveHolidayLocale, DEFAULT_HOLIDAY_LOCALE, HOLIDAY_COUNTRY_CODES } from './holiday-locale.js'
19
19
  export { Timesheet } from './Timesheet.jsx'
20
20
  export { TimesheetExportCard } from './TimesheetExportCard.jsx'
21
21
  export { TimesheetsFreeTool } from './TimesheetsFreeTool.jsx'
@@ -32,8 +32,9 @@
32
32
  "timesheets.detail.description": "Granska och justera timmar, spara eller exportera.",
33
33
  "timesheets.new": "Ny",
34
34
  "timesheets.createTitle": "Ny tidrapport",
35
- "timesheets.createDescription": "Välj en månad för att generera en förifylld tidrapport.",
35
+ "timesheets.createDescription": "Välj en månad och helgdagskalender för att generera en förifylld tidrapport.",
36
36
  "timesheets.period": "Period",
37
+ "timesheets.holidayLocale": "Helgdagskalender",
37
38
  "timesheets.generate": "Generera",
38
39
  "timesheets.openExisting": "Öppna",
39
40
  "timesheets.periodExistsHint": "Det finns redan en tidrapport för perioden — Öppna laddar den.",