@dnax/ui 0.0.70 → 0.0.71

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.
@@ -4,7 +4,8 @@
4
4
  // édition en place (Entrée / F2 / double-clic / frappe directe),
5
5
  // lignes & colonnes (ajouter / insérer / supprimer),
6
6
  // tri, copier/coller, undo/redo, et cellules typées
7
- // (texte, nombre, entier, booléen, date, datetime, select / multiselect avec badges colorés).
7
+ // texte, nombre, entier, booléen, date, datetime, **time (heure)**, select / multiselect avec
8
+ // badges colorés).
8
9
  // Suivi des modifications : `v-model:dirty` (état) + `v-model:changes` (delta lignes / feuilles
9
10
  // ajoutées, modifiées, supprimées), calculé en comparant le document à une **référence**
10
11
  // (`lib/spreadsheetChanges.ts`).
@@ -44,6 +45,7 @@ export type QSpreadsheetCellType =
44
45
  | "boolean"
45
46
  | "date"
46
47
  | "datetime"
48
+ | "time"
47
49
  | "select"
48
50
  | "multiselect"
49
51
 
@@ -185,6 +187,9 @@ import {
185
187
  isBlankValue,
186
188
  ISO_DATE,
187
189
  isoAddDays,
190
+ normalizeTime,
191
+ secondsToTime,
192
+ timeToSeconds,
188
193
  shiftFormulaRefs,
189
194
  functionQueryAt,
190
195
  rankSuggestions,
@@ -877,6 +882,8 @@ const cellText = (row: number, column: string): string => {
877
882
  let out: any = ev === null || ev === undefined ? "" : ev
878
883
  // Apostrophe littérale (paste values) : affichée sans le préfixe
879
884
  if (typeof out === "string" && out.startsWith("'")) out = out.slice(1)
885
+ // Heure : toujours `HH:MM` à l'affichage (données importées ou anciennes comprises)
886
+ if (col?.type === "time" && typeof out === "string") out = normalizeTime(out) ?? out
880
887
  if (col?.format && out !== "") out = col.format(out, state.value[row] ?? {})
881
888
  if (out === null || out === undefined || out === "") return ""
882
889
  return String(out)
@@ -988,6 +995,8 @@ const editorInputType = computed(() => {
988
995
  return "date"
989
996
  case "datetime":
990
997
  return "datetime-local"
998
+ case "time":
999
+ return "time"
991
1000
  default:
992
1001
  // PAS de `type="number"` pour number/integer : (1) le navigateur refuse
993
1002
  // `=` (formule) ou un "-" isolé dans un input number, (2) `v-model` sur
@@ -1022,6 +1031,8 @@ const startEdit = (row: number, column: string, initial?: string) => {
1022
1031
  } else {
1023
1032
  let rawStr = String(raw ?? "")
1024
1033
  if (rawStr.startsWith("'")) rawStr = rawStr.slice(1)
1034
+ // Heure : l'input natif n'accepte que `HH:MM` — on normalise dès l'ouverture
1035
+ if (col?.type === "time") rawStr = normalizeTime(rawStr) ?? rawStr
1025
1036
  draft.value = String(initial ?? rawStr)
1026
1037
  }
1027
1038
  editing.value = { row, column }
@@ -1099,6 +1110,11 @@ const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: an
1099
1110
  if (col?.type === "boolean") {
1100
1111
  return raw === "true" ? true : raw === "false" ? false : old
1101
1112
  }
1113
+ if (col?.type === "time") {
1114
+ // Heure valide → `HH:MM` (`HH:MM:SS` si les secondes sont non nulles) ; invalide → ancienne
1115
+ const time = normalizeTime(raw)
1116
+ return time === undefined ? old : time
1117
+ }
1102
1118
  if (col?.type === "select") {
1103
1119
  const opt = cellOptionsOf(col).find(
1104
1120
  (o) => String(o.label ?? "") === raw || String(o.value) === raw,
@@ -3065,6 +3081,19 @@ const applyFill = () => {
3065
3081
  const d1 = +new Date(s1 + "T00:00:00Z")
3066
3082
  const step = Math.round((d1 - d0) / 86400000)
3067
3083
  if (step !== 0) val = isoAddDays(s1, step * (rr - b.r1))
3084
+ } else if (
3085
+ b.r1 > b.r0 &&
3086
+ col?.type === "time" &&
3087
+ typeof s0 === "string" &&
3088
+ typeof s1 === "string"
3089
+ ) {
3090
+ // Série d'heures : 09:00 puis 10:00 → 11:00, 12:00… (modulo 24 h)
3091
+ const t0 = timeToSeconds(s0)
3092
+ const t1 = timeToSeconds(s1)
3093
+ if (t0 !== undefined && t1 !== undefined && t1 !== t0) {
3094
+ const step = (t1 - t0) / (b.r1 - b.r0)
3095
+ val = secondsToTime(t1 + step * (rr - b.r1))
3096
+ }
3068
3097
  }
3069
3098
  } else if (
3070
3099
  !fillCopy.value &&
@@ -3081,6 +3110,18 @@ const applyFill = () => {
3081
3110
  if (b.c1 > b.c0 && typeof s0 === "number" && typeof s1 === "number" && s1 !== s0) {
3082
3111
  const step = (s1 - s0) / (b.c1 - b.c0)
3083
3112
  val = s1 + step * (cc - b.c1)
3113
+ } else if (
3114
+ b.c1 > b.c0 &&
3115
+ col?.type === "time" &&
3116
+ typeof s0 === "string" &&
3117
+ typeof s1 === "string"
3118
+ ) {
3119
+ const t0 = timeToSeconds(s0)
3120
+ const t1 = timeToSeconds(s1)
3121
+ if (t0 !== undefined && t1 !== undefined && t1 !== t0) {
3122
+ const step = (t1 - t0) / (b.c1 - b.c0)
3123
+ val = secondsToTime(t1 + step * (cc - b.c1))
3124
+ }
3084
3125
  }
3085
3126
  }
3086
3127
  // Recopie du motif (source cyclique) — vers le bas, la droite, le haut,
@@ -14,8 +14,47 @@ import {
14
14
  csvSplitLine,
15
15
  parseCsv,
16
16
  deriveCols,
17
+ normalizeTime,
18
+ parseTimeOfDay,
19
+ secondsToTime,
20
+ timeToSeconds,
17
21
  } from "./spreadsheet"
18
22
 
23
+ describe("heures (type `time`)", () => {
24
+ it("décompose les formes usuelles", () => {
25
+ expect(parseTimeOfDay("09:30")).toEqual({ hours: 9, minutes: 30, seconds: 0 })
26
+ expect(parseTimeOfDay("9h5")).toEqual({ hours: 9, minutes: 5, seconds: 0 })
27
+ expect(parseTimeOfDay("9")).toEqual({ hours: 9, minutes: 0, seconds: 0 })
28
+ expect(parseTimeOfDay("23:59:58")).toEqual({ hours: 23, minutes: 59, seconds: 58 })
29
+ // vide → cellule effacée ; null/undefined → pas une heure
30
+ expect(parseTimeOfDay("")).toBeNull()
31
+ expect(parseTimeOfDay(null)).toBeUndefined()
32
+ })
33
+
34
+ it("refuse les heures invalides", () => {
35
+ for (const value of ["24:00", "9:75", "9:30:75", "abc", "1:2:3:4", "-1:00"]) {
36
+ expect(parseTimeOfDay(value), value).toBeUndefined()
37
+ }
38
+ })
39
+
40
+ it("normalise en `HH:MM` (secondes conservées si non nulles)", () => {
41
+ expect(normalizeTime("9:5")).toBe("09:05")
42
+ expect(normalizeTime("9h30")).toBe("09:30")
43
+ expect(normalizeTime("09:30:00")).toBe("09:30")
44
+ expect(normalizeTime("09:30:07")).toBe("09:30:07")
45
+ expect(normalizeTime("")).toBeNull()
46
+ expect(normalizeTime("nope")).toBeUndefined()
47
+ })
48
+
49
+ it("séries d'heures : secondes, modulo 24 h, valeurs négatives", () => {
50
+ expect(timeToSeconds("01:00")).toBe(3600)
51
+ expect(secondsToTime(3600)).toBe("01:00")
52
+ expect(secondsToTime(25 * 3600)).toBe("01:00")
53
+ expect(secondsToTime(-3600)).toBe("23:00")
54
+ expect(secondsToTime(3661)).toBe("01:01:01")
55
+ })
56
+ })
57
+
19
58
  describe("lettres de colonnes", () => {
20
59
  it("colLetter : 0→A, 25→Z, 26→AA, 27→AB, 701→ZZ, 702→AAA", () => {
21
60
  expect(colLetter(0)).toBe("A")
@@ -247,6 +247,74 @@ export const isoAddDays = (iso: string, days: number): string => {
247
247
  return d.toISOString().slice(0, 10)
248
248
  }
249
249
 
250
+ /** Heure du jour : `HH:MM` ou `HH:MM:SS` (toujours sur deux chiffres) */
251
+ export const TIME_OF_DAY = /^\d{2}:\d{2}(?::\d{2})?$/
252
+
253
+ /**
254
+ * Décompose une **heure** saisie librement. Accepte `9`, `09:30`, `9:5`, `9h30`, `09:30:15`.
255
+ *
256
+ * - `null` → chaîne vide (cellule effacée) ;
257
+ * - `undefined` → ce n'est pas une heure valide (`24:00`, `9:75`, `abc`…).
258
+ *
259
+ * Le parsing est volontairement **local** (pas de `Date`) : aucune conversion de fuseau ne
260
+ * doit intervenir sur une heure de la journée.
261
+ */
262
+ export function parseTimeOfDay(
263
+ value: any,
264
+ ): { hours: number; minutes: number; seconds: number } | null | undefined {
265
+ if (value === null || value === undefined) return undefined
266
+ const raw = String(value).trim()
267
+ if (raw === "") return null
268
+ const match = raw.match(/^(\d{1,2})(?:[:h](\d{1,2}))?(?::(\d{1,2}))?$/i)
269
+ if (!match) return undefined
270
+ const hours = Number(match[1])
271
+ const minutes = match[2] === undefined ? 0 : Number(match[2])
272
+ const seconds = match[3] === undefined ? 0 : Number(match[3])
273
+ if (
274
+ !Number.isInteger(hours) ||
275
+ !Number.isInteger(minutes) ||
276
+ !Number.isInteger(seconds) ||
277
+ hours > 23 ||
278
+ minutes > 59 ||
279
+ seconds > 59
280
+ ) {
281
+ return undefined
282
+ }
283
+ return { hours, minutes, seconds }
284
+ }
285
+
286
+ const two = (n: number) => String(n).padStart(2, "0")
287
+
288
+ /**
289
+ * Normalise une heure en `HH:MM` — les secondes ne sont conservées que si elles sont non
290
+ * nulles (`HH:MM:SS`). `null` = vide, `undefined` = invalide (l'appelant garde l'ancienne
291
+ * valeur, comme pour un nombre refusé).
292
+ */
293
+ export function normalizeTime(value: any): string | null | undefined {
294
+ const parsed = parseTimeOfDay(value)
295
+ if (parsed === null || parsed === undefined) return parsed
296
+ const base = `${two(parsed.hours)}:${two(parsed.minutes)}`
297
+ return parsed.seconds ? `${base}:${two(parsed.seconds)}` : base
298
+ }
299
+
300
+ /** Secondes depuis minuit (séries d'auto-remplissage) — `undefined` si invalide */
301
+ export function timeToSeconds(value: any): number | undefined {
302
+ const parsed = parseTimeOfDay(value)
303
+ if (!parsed) return undefined
304
+ return parsed.hours * 3600 + parsed.minutes * 60 + parsed.seconds
305
+ }
306
+
307
+ /** Secondes depuis minuit → `HH:MM` (`HH:MM:SS` si nécessaire), **modulo 24 h** */
308
+ export function secondsToTime(total: number): string {
309
+ if (!Number.isFinite(total)) return "00:00"
310
+ const day = 86400
311
+ const normalized = ((Math.round(total) % day) + day) % day
312
+ const hours = Math.floor(normalized / 3600)
313
+ const minutes = Math.floor((normalized % 3600) / 60)
314
+ const seconds = normalized % 60
315
+ return seconds ? `${two(hours)}:${two(minutes)}:${two(seconds)}` : `${two(hours)}:${two(minutes)}`
316
+ }
317
+
250
318
  /** Échappe une valeur pour un CSV (quote si délimiteur / guillemet / newline) */
251
319
  export const csvEscape = (s: unknown, delim: string): string => {
252
320
  const str = s === null || s === undefined ? "" : String(s)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.70",
3
+ "version": "0.0.71",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",