@flamingo-stack/openframe-frontend-core 0.0.739 → 0.0.741

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 (197) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-ENCAIAMY.cjs → chunk-4XL4YEZM.cjs} +3 -3
  3. package/dist/{chunk-FL6UO7IR.cjs → chunk-5SVOGLNK.cjs} +2 -2
  4. package/dist/{chunk-WFPCCBOM.cjs → chunk-7AM3AFM4.cjs} +10 -10
  5. package/dist/{chunk-S2CA5WE4.cjs → chunk-CWRVTQUO.cjs} +9 -1
  6. package/dist/{chunk-KHXZFLIN.cjs → chunk-DKOLQPE7.cjs} +8 -2
  7. package/dist/{chunk-UXFJXIUT.cjs → chunk-EKGL5K5K.cjs} +2 -2
  8. package/dist/{chunk-DTLHY2ZF.cjs → chunk-FQN6STIM.cjs} +12 -155
  9. package/dist/{chunk-V2ZFH42N.cjs → chunk-HYBMADYO.cjs} +3 -3
  10. package/dist/{chunk-NX6JQT25.cjs → chunk-I6UE6YVM.cjs} +4 -4
  11. package/dist/{chunk-SB4VWA5V.cjs → chunk-IK2RJUEA.cjs} +2118 -268
  12. package/dist/{chunk-CUDTMCXK.cjs → chunk-ITPEFCQ2.cjs} +13 -13
  13. package/dist/{chunk-WMKNZI4G.cjs → chunk-JFS6TZDX.cjs} +22 -22
  14. package/dist/{chunk-RGFT3P3L.cjs → chunk-LZ6NOVXX.cjs} +55 -55
  15. package/dist/{chunk-TF7J6VXG.cjs → chunk-QEUGYHFB.cjs} +9 -9
  16. package/dist/{chunk-II4QMH26.cjs → chunk-STEJZPIO.cjs} +3 -3
  17. package/dist/{chunk-UI3HROSI.cjs → chunk-XJ36TEGE.cjs} +33 -33
  18. package/dist/{chunk-UYP3G6PB.cjs → chunk-YQ37ZCD2.cjs} +2 -2
  19. package/dist/{chunk-Q7GG3WC5.cjs → chunk-ZJPDGVA6.cjs} +5 -5
  20. package/dist/{chunk-EGN4I5Q7.cjs → chunk-ZYOS7LVQ.cjs} +134 -46
  21. package/dist/{cloud-tenants-QSFSO3PA.cjs → cloud-tenants-MVJK52I2.cjs} +13 -13
  22. package/dist/components/case-studies/index.cjs +11 -11
  23. package/dist/components/chat/index.cjs +2 -2
  24. package/dist/components/contact/index.cjs +4 -4
  25. package/dist/components/docs/index.cjs +7 -7
  26. package/dist/components/embeds/index.cjs +4 -4
  27. package/dist/components/faq/index.cjs +5 -5
  28. package/dist/components/features/capability-explorer.d.ts +9 -2
  29. package/dist/components/features/capability-explorer.d.ts.map +1 -1
  30. package/dist/components/features/capability-explorer.js +113 -6
  31. package/dist/components/features/index.cjs +141 -3
  32. package/dist/components/features/index.d.ts +1 -0
  33. package/dist/components/features/index.d.ts.map +1 -1
  34. package/dist/components/features/index.js +1 -0
  35. package/dist/components/features/scripts/execution-cells.d.ts +65 -0
  36. package/dist/components/features/scripts/execution-cells.d.ts.map +1 -0
  37. package/dist/components/features/scripts/execution-cells.js +75 -0
  38. package/dist/components/features/scripts/execution-status.d.ts +48 -0
  39. package/dist/components/features/scripts/execution-status.d.ts.map +1 -0
  40. package/dist/components/features/scripts/execution-status.js +15 -0
  41. package/dist/components/features/scripts/index.d.ts +20 -0
  42. package/dist/components/features/scripts/index.d.ts.map +1 -0
  43. package/dist/components/features/scripts/index.js +59 -0
  44. package/dist/components/features/scripts/schedule-form/index.d.ts +18 -0
  45. package/dist/components/features/scripts/schedule-form/index.d.ts.map +1 -0
  46. package/dist/components/features/scripts/schedule-form/index.js +84 -0
  47. package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts +24 -0
  48. package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts.map +1 -0
  49. package/dist/components/features/scripts/schedule-form/schedule-enums.js +18 -0
  50. package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts +57 -0
  51. package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts.map +1 -0
  52. package/dist/components/features/scripts/schedule-form/schedule-form-fields.js +169 -0
  53. package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts +138 -0
  54. package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts.map +1 -0
  55. package/dist/components/features/scripts/schedule-form/schedule-form-types.js +72 -0
  56. package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts +54 -0
  57. package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts.map +1 -0
  58. package/dist/components/features/scripts/schedule-form/schedule-interval-input.js +60 -0
  59. package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts +29 -0
  60. package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts.map +1 -0
  61. package/dist/components/features/scripts/schedule-form/schedule-offline-fields.js +124 -0
  62. package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts +52 -0
  63. package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts.map +1 -0
  64. package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.js +244 -0
  65. package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts +35 -0
  66. package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts.map +1 -0
  67. package/dist/components/features/scripts/schedule-form/schedule-script-search-field.js +58 -0
  68. package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts +25 -0
  69. package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts.map +1 -0
  70. package/dist/components/features/scripts/schedule-form/schedule-scripts-field.js +66 -0
  71. package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts +77 -0
  72. package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts.map +1 -0
  73. package/dist/components/features/scripts/schedule-form/schedule-timing-fields.js +262 -0
  74. package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts +288 -0
  75. package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts.map +1 -0
  76. package/dist/components/features/scripts/schedule-form/schedule-timing.js +165 -0
  77. package/dist/components/features/scripts/script-cells.d.ts +11 -0
  78. package/dist/components/features/scripts/script-cells.d.ts.map +1 -0
  79. package/dist/components/features/scripts/script-cells.js +19 -0
  80. package/dist/components/features/scripts/script-executions-table.d.ts +58 -0
  81. package/dist/components/features/scripts/script-executions-table.d.ts.map +1 -0
  82. package/dist/components/features/scripts/script-executions-table.js +88 -0
  83. package/dist/components/features/scripts/script-schedules-table.d.ts +38 -0
  84. package/dist/components/features/scripts/script-schedules-table.d.ts.map +1 -0
  85. package/dist/components/features/scripts/script-schedules-table.js +71 -0
  86. package/dist/components/features/scripts/script-shell-badge.d.ts +9 -0
  87. package/dist/components/features/scripts/script-shell-badge.d.ts.map +1 -0
  88. package/dist/components/features/scripts/script-shell-badge.js +15 -0
  89. package/dist/components/features/scripts/scripts-table-columns.d.ts +162 -0
  90. package/dist/components/features/scripts/scripts-table-columns.d.ts.map +1 -0
  91. package/dist/components/features/scripts/scripts-table-columns.js +69 -0
  92. package/dist/components/features/scripts/scripts-table-shared.d.ts +19 -0
  93. package/dist/components/features/scripts/scripts-table-shared.d.ts.map +1 -0
  94. package/dist/components/features/scripts/scripts-table-shared.js +35 -0
  95. package/dist/components/features/scripts/scripts-table.d.ts +40 -0
  96. package/dist/components/features/scripts/scripts-table.d.ts.map +1 -0
  97. package/dist/components/features/scripts/scripts-table.js +84 -0
  98. package/dist/components/features/scripts/scripts-tabs.d.ts +5 -0
  99. package/dist/components/features/scripts/scripts-tabs.d.ts.map +1 -0
  100. package/dist/components/features/scripts/scripts-tabs.js +10 -0
  101. package/dist/components/features/scripts/shell-types.d.ts +13 -0
  102. package/dist/components/features/scripts/shell-types.d.ts.map +1 -0
  103. package/dist/components/features/scripts/shell-types.js +21 -0
  104. package/dist/components/help-center-pages/index.cjs +53 -53
  105. package/dist/components/index.cjs +178 -34
  106. package/dist/components/meeting-scheduler/index.cjs +17 -17
  107. package/dist/components/navigation/index.cjs +6 -6
  108. package/dist/components/onboarding-guides/index.cjs +5 -5
  109. package/dist/components/product-demo/fixtures/scripts.d.ts +34 -0
  110. package/dist/components/product-demo/fixtures/scripts.d.ts.map +1 -0
  111. package/dist/components/product-demo/fixtures/scripts.js +123 -0
  112. package/dist/components/product-demo/fixtures/shared.d.ts +8 -0
  113. package/dist/components/product-demo/fixtures/shared.d.ts.map +1 -1
  114. package/dist/components/product-demo/fixtures/shared.js +7 -1
  115. package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
  116. package/dist/components/product-demo/fixtures/software-update.js +1 -5
  117. package/dist/components/product-demo/index.cjs +16 -13
  118. package/dist/components/product-demo/product-screen.d.ts.map +1 -1
  119. package/dist/components/product-demo/product-screen.js +2 -1
  120. package/dist/components/product-demo/registry.d.ts.map +1 -1
  121. package/dist/components/product-demo/registry.js +2 -1
  122. package/dist/components/product-demo/screen-keys.cjs +2 -1
  123. package/dist/components/product-demo/screen-keys.d.ts +1 -0
  124. package/dist/components/product-demo/screen-keys.d.ts.map +1 -1
  125. package/dist/components/product-demo/screen-keys.js +2 -1
  126. package/dist/components/product-demo/screens/scripts.d.ts +11 -0
  127. package/dist/components/product-demo/screens/scripts.d.ts.map +1 -0
  128. package/dist/components/product-demo/screens/scripts.js +130 -0
  129. package/dist/components/shared/legal-document/index.d.ts +2 -1
  130. package/dist/components/shared/legal-document/index.d.ts.map +1 -1
  131. package/dist/components/shared/legal-document/index.js +10 -2
  132. package/dist/components/shared/legal-document/legal-document-page.d.ts +35 -23
  133. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  134. package/dist/components/shared/legal-document/legal-document-page.js +27 -10
  135. package/dist/components/shared/legal-document/use-legal-docs.d.ts +7 -3
  136. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
  137. package/dist/components/shared/legal-document/use-legal-docs.js +7 -0
  138. package/dist/components/shared/legal-document/use-legal-documents.d.ts +31 -0
  139. package/dist/components/shared/legal-document/use-legal-documents.d.ts.map +1 -0
  140. package/dist/components/shared/legal-document/use-legal-documents.js +67 -0
  141. package/dist/components/tickets/index.cjs +8 -8
  142. package/dist/components/ui/index.cjs +3 -3
  143. package/dist/{devices-C7XLWJFC.cjs → devices-W3S7Q2PG.cjs} +9 -9
  144. package/dist/index.cjs +150 -8
  145. package/dist/{knowledge-3QKWTOE7.cjs → knowledge-AIYQ4FC3.cjs} +7 -7
  146. package/dist/{logs-JXD75NR4.cjs → logs-7SHJ2ARE.cjs} +25 -25
  147. package/dist/{policies-ZPN53PFQ.cjs → policies-EIZJ5SUP.cjs} +10 -10
  148. package/dist/{remote-session-DNFZ5OLY.cjs → remote-session-BZMNE6D2.cjs} +5 -5
  149. package/dist/scripts-J2FWC7E4.cjs +2112 -0
  150. package/dist/{software-update-CRSIBS3K.cjs → software-update-QAMNASVF.cjs} +14 -18
  151. package/dist/{tickets-board-CKZRB6YV.cjs → tickets-board-CEDHD3KB.cjs} +17 -17
  152. package/dist/utils/index.cjs +8 -0
  153. package/dist/utils/index.d.ts +1 -0
  154. package/dist/utils/index.d.ts.map +1 -1
  155. package/dist/utils/index.js +1 -0
  156. package/dist/utils/legal-documents.d.ts +18 -0
  157. package/dist/utils/legal-documents.d.ts.map +1 -0
  158. package/dist/utils/legal-documents.js +8 -0
  159. package/package.json +1 -1
  160. package/src/components/features/capability-explorer.tsx +134 -7
  161. package/src/components/features/index.ts +1 -0
  162. package/src/components/features/scripts/execution-cells.tsx +149 -0
  163. package/src/components/features/scripts/execution-status.ts +42 -0
  164. package/src/components/features/scripts/index.ts +42 -0
  165. package/src/components/features/scripts/schedule-form/index.ts +59 -0
  166. package/src/components/features/scripts/schedule-form/schedule-enums.ts +26 -0
  167. package/src/components/features/scripts/schedule-form/schedule-form-fields.tsx +249 -0
  168. package/src/components/features/scripts/schedule-form/schedule-form-types.ts +189 -0
  169. package/src/components/features/scripts/schedule-form/schedule-interval-input.tsx +121 -0
  170. package/src/components/features/scripts/schedule-form/schedule-offline-fields.tsx +218 -0
  171. package/src/components/features/scripts/schedule-form/schedule-script-picker-card.tsx +321 -0
  172. package/src/components/features/scripts/schedule-form/schedule-script-search-field.tsx +115 -0
  173. package/src/components/features/scripts/schedule-form/schedule-scripts-field.tsx +97 -0
  174. package/src/components/features/scripts/schedule-form/schedule-timing-fields.tsx +482 -0
  175. package/src/components/features/scripts/schedule-form/schedule-timing.ts +484 -0
  176. package/src/components/features/scripts/script-cells.tsx +29 -0
  177. package/src/components/features/scripts/script-executions-table.tsx +145 -0
  178. package/src/components/features/scripts/script-schedules-table.tsx +113 -0
  179. package/src/components/features/scripts/script-shell-badge.tsx +25 -0
  180. package/src/components/features/scripts/scripts-table-columns.ts +95 -0
  181. package/src/components/features/scripts/scripts-table-shared.tsx +43 -0
  182. package/src/components/features/scripts/scripts-table.tsx +124 -0
  183. package/src/components/features/scripts/scripts-tabs.ts +11 -0
  184. package/src/components/features/scripts/shell-types.ts +26 -0
  185. package/src/components/product-demo/fixtures/scripts.ts +186 -0
  186. package/src/components/product-demo/fixtures/shared.ts +7 -0
  187. package/src/components/product-demo/fixtures/software-update.ts +1 -7
  188. package/src/components/product-demo/product-screen.tsx +3 -1
  189. package/src/components/product-demo/registry.ts +1 -0
  190. package/src/components/product-demo/screen-keys.ts +1 -0
  191. package/src/components/product-demo/screens/scripts.tsx +192 -0
  192. package/src/components/shared/legal-document/index.ts +8 -0
  193. package/src/components/shared/legal-document/legal-document-page.tsx +83 -44
  194. package/src/components/shared/legal-document/use-legal-docs.ts +18 -3
  195. package/src/components/shared/legal-document/use-legal-documents.ts +113 -0
  196. package/src/utils/index.ts +1 -0
  197. package/src/utils/legal-documents.ts +21 -0
@@ -0,0 +1,482 @@
1
+ 'use client';
2
+ 'use no memo';
3
+
4
+ import { useCallback, useMemo } from 'react';
5
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
6
+ import { cn } from '../../../../utils/cn';
7
+ import { XmarkCircleIcon } from '../../../icons-v2-generated/signs-and-symbols/xmark-circle-icon';
8
+ import { CheckboxBlock } from '../../../ui/checkbox-block';
9
+ import { DatePickerInputSimple } from '../../../ui/date-picker';
10
+ import { Label } from '../../../ui/label';
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select';
12
+ import type { ScheduleTimeReference } from './schedule-enums';
13
+ import {
14
+ SCHEDULE_TIME_REFERENCE_OPTIONS,
15
+ type ScheduleFormData,
16
+ type ScheduleFormGate,
17
+ type ScheduleTimeSlot,
18
+ } from './schedule-form-types';
19
+ import { ScheduleIntervalInput } from './schedule-interval-input';
20
+ import {
21
+ DURATION_UNIT_OPTIONS,
22
+ type DurationUnit,
23
+ earliestScheduleDay,
24
+ getTimeSlotOptions,
25
+ isEventTrigger,
26
+ isScheduleStartInPast,
27
+ isSlotOnGrid,
28
+ isStartInPastAndChanged,
29
+ MIN_REPEAT_MINUTES,
30
+ NO_SLOTS_TODAY_MESSAGE,
31
+ PAST_START_MESSAGE,
32
+ slotToLabel,
33
+ snapRepeatInterval,
34
+ } from './schedule-timing';
35
+
36
+ export interface ScheduleTimingFieldsProps {
37
+ showErrors: boolean;
38
+ disabled?: boolean;
39
+ /**
40
+ * Whether the Timezone picker is offered. It is a layout switch (it turns one
41
+ * row into two), so a host that reads it from a flag passes the flag's GATE.
42
+ * Default `off`.
43
+ */
44
+ timeReferenceGate?: ScheduleFormGate;
45
+ /**
46
+ * The Time dropdown's options for a picked day and reading. Default
47
+ * `getTimeSlotOptions`: the viewer's own clock and locale. A picture of the
48
+ * form passes fixed readings, so a server and a browser draw the same one.
49
+ */
50
+ getTimeSlots?: (date: Date | null, timeReference: ScheduleTimeReference) => ScheduleTimeSlot[];
51
+ }
52
+
53
+ /**
54
+ * Date | Time | Timezone | Repeat | Repeat in — the timing a DATE_TIME schedule
55
+ * fires on.
56
+ *
57
+ * `scheduledDate` + `scheduledTime` combine into the backend's `startAt` (30-min
58
+ * boundary, enforced by the Time slots) and are both required; the repeat toggle
59
+ * + interval + unit become `repeat` seconds.
60
+ *
61
+ * **Timezone** (`timeReference`, design node 793:61340) says what that pair
62
+ * MEANS — the account's clock (SERVER, one instant worldwide) or each device's
63
+ * (DEVICE_LOCAL, one reading re-based per device). It is behind the
64
+ * host's `timeReferenceGate` (the product's `script-schedule-device-time`
65
+ * flag), and it is the field that makes this a
66
+ * five-control row: with the picker on, the design wraps Repeat onto a second
67
+ * line, which is why the row is a 4-column GRID rather than the flex row it
68
+ * used to be — the wrap is then a `col-start-1` on one cell instead of a second
69
+ * layout.
70
+ *
71
+ * Recurrence stays OFFERED for DEVICE_LOCAL — "every day at 9 AM on each
72
+ * device's own clock" is the reason the reading exists — even though the API
73
+ * refuses it today ("A DEVICE_LOCAL schedule does not support repeat **yet**",
74
+ * `ScheduleScriptService.validateTiming`). Saving one is answered by that
75
+ * message in the error toast until the backend drops the check; nothing here
76
+ * has to change when it does. Its offline setting is refused the same way and
77
+ * IS hidden, because that one is not merely unimplemented: the device-local
78
+ * runner already retries on reconnect within its catch-up window, so the
79
+ * setting would have nothing to add — see `ScheduleOfflineFields`.
80
+ *
81
+ * What it does NOT constrain is the clock. The picked pair is a wall clock every
82
+ * device reads on its own timezone, so the viewer's "already gone by" says
83
+ * nothing about the fleet's: all 48 slots stay offered, the past-start rule is
84
+ * off, and the only bound left is the day one (`earliestScheduleDay` — today as
85
+ * read in UTC−12). The API validates no start against the past either, for
86
+ * either reading.
87
+ *
88
+ * Only DATE_TIME schedules have timing, so the row collapses when the event
89
+ * trigger is picked. It stays MOUNTED (a toggle back restores what was typed)
90
+ * and animates with the `0fr → 1fr` grid-rows technique — the portable way to
91
+ * transition to an intrinsic height, since `interpolate-size` / `calc-size()`
92
+ * still aren't cross-browser. `inert` drops the collapsed fields out of the tab
93
+ * order and the accessibility tree.
94
+ *
95
+ * The `overflow-hidden` the technique requires also clips anything hanging below
96
+ * the fields — and field errors are absolutely positioned there (out of flow, so
97
+ * showing one never reflows the form). The bottom padding is the room they render
98
+ * into; the constant negative margin cancels it again, so the spacing below the
99
+ * row is unchanged and a COLLAPSED row leaves no 24px hole.
100
+ *
101
+ * That padding sits on the field row INSIDE the clipping box, not on the box
102
+ * itself, and the difference is load-bearing: a box is never shorter than its own
103
+ * padding — `border-box` does not change that, it only decides what a specified
104
+ * height includes — so padding on the grid item survives the 0fr track as 24px of
105
+ * residue. The negative margin then spends itself cancelling that instead of
106
+ * closing the section gap, and the collapsed row leaves the hole this was written
107
+ * to avoid. Nested one level down it is clipped like everything else, and the row
108
+ * measures 0.
109
+ */
110
+ export function ScheduleTimingFields({
111
+ showErrors,
112
+ disabled = false,
113
+ timeReferenceGate = 'off',
114
+ getTimeSlots = getTimeSlotOptions,
115
+ }: ScheduleTimingFieldsProps) {
116
+ const { control, getValues, setValue, trigger: triggerValidation } = useFormContext<ScheduleFormData>();
117
+
118
+ // The Timezone picker is a layout switch — it turns one row into two — so the
119
+ // flag is read as a GATE and the unanswered window is not guessed at. There is
120
+ // nothing to draw for it (a picker shown and then withdrawn is exactly what a
121
+ // both-directions gate must not do), so the window is spent with the row
122
+ // LOCKED instead: the same treatment the page already gives fields whose
123
+ // record has not landed, and it means the control cannot appear beside a
124
+ // choice the user has already made.
125
+ const showTimeReference = timeReferenceGate === 'on';
126
+ const fieldsDisabled = disabled || timeReferenceGate === 'loading';
127
+
128
+ /**
129
+ * Re-checks the reconnect window after the cadence moves.
130
+ *
131
+ * The rule "the window must be shorter than the cadence" is attached to
132
+ * `reconnectInterval`, and react-hook-form only refreshes the error of the
133
+ * field that just fired an event — so fixing the CADENCE left the complaint
134
+ * sitting under the offline block until the next Save, pointing at a value
135
+ * that was already legal.
136
+ *
137
+ * Called on the cadence's blur and on its two discrete controls, never
138
+ * per-keystroke: the half-typed "6" of "60" is briefly shorter than the
139
+ * window, and grading it would flash an error at a number still being
140
+ * written. Silent until the first Save, like every other rule here.
141
+ */
142
+ const recheckReconnectWindow = useCallback(() => {
143
+ if (showErrors) void triggerValidation('reconnectInterval');
144
+ }, [showErrors, triggerValidation]);
145
+ const trigger = useWatch({ control, name: 'trigger' });
146
+ const repeatEnabled = useWatch({ control, name: 'repeatEnabled' });
147
+ const repeatUnit = useWatch({ control, name: 'repeatUnit' });
148
+ const scheduledDate = useWatch({ control, name: 'scheduledDate' });
149
+ const scheduledTime = useWatch({ control, name: 'scheduledTime' });
150
+ const timeReference = useWatch({ control, name: 'timeReference' });
151
+ const eventDriven = isEventTrigger(trigger);
152
+ // Minutes are the one unit that can express a cadence finer than the runner's
153
+ // 30-minute grid, so they are the one unit the stepper has to constrain — it
154
+ // then produces only legal values, and the schema rule behind it is left to
155
+ // catch typed-in ones.
156
+ const intervalStep = repeatUnit === 'minute' ? MIN_REPEAT_MINUTES : 1;
157
+ // Local slots, so the grid depends on the viewer's timezone — built per mount
158
+ // rather than at import time, which would happen on the server, and rebuilt
159
+ // per picked DAY because today offers only the slots still ahead.
160
+ //
161
+ // A stored start that has already gone by today keeps its option: it is the
162
+ // value the form holds and will save if nothing else changes, and a Select
163
+ // whose value is missing from its list renders as the placeholder — the field
164
+ // would read empty on a schedule that has a perfectly good start time.
165
+ //
166
+ // The grid itself also depends on the READING: a DEVICE_LOCAL start stores its
167
+ // wall clock verbatim, so the boundary applies to the picked digits and the
168
+ // slots are the plain xx:00 / xx:30 — which differs from the SERVER grid in
169
+ // the 45-minute zones, and only there.
170
+ const timeSlots = useMemo(() => {
171
+ const slots = getTimeSlots(scheduledDate, timeReference);
172
+ if (!scheduledTime || slots.some(slot => slot.value === scheduledTime)) return slots;
173
+ return [{ value: scheduledTime, label: slotToLabel(scheduledTime) }, ...slots];
174
+ }, [getTimeSlots, scheduledDate, scheduledTime, timeReference]);
175
+ // Today, recomputed per mount so a tab left open overnight cannot still treat
176
+ // yesterday as selectable. Handed to the picker as `fromDate`, which greys out
177
+ // every earlier day and stops the calendar paging past it.
178
+ //
179
+ // ⚠ Requires a core library that TRANSLATES that prop. Up to and including
180
+ // 0.0.514 the calendar forwarded it straight to react-day-picker v9, where
181
+ // `fromDate` was removed — the bound was silently dropped and past days stayed
182
+ // clickable. Fixed in core (`DatePickerCalendar` maps it to a `disabled`
183
+ // matcher); if this app is ever pinned back below that release, the two rules
184
+ // below are what still hold the line.
185
+ //
186
+ // They are not a fallback in any case: a disabled day cannot stop the clock
187
+ // from passing the SLOT this form already holds, and the seeded value never
188
+ // went through the calendar at all.
189
+ //
190
+ // DEVICE_LOCAL moves the bound west instead of dropping it: a wall clock is
191
+ // gone only once it is gone on every clock on Earth, so the floor is today as
192
+ // read in UTC−12 (`earliestScheduleDay`). Depends on the reading, so it is
193
+ // rebuilt when that changes — and on nothing else, so a tab left open still
194
+ // cannot walk the bound backwards past its own mount.
195
+ const minDate = useMemo(() => earliestScheduleDay(timeReference), [timeReference]);
196
+
197
+ /**
198
+ * Moving the DAY can invalidate the time already chosen: 8:00 AM is a fine
199
+ * slot for tomorrow and a gone one for today. The dropdown stops offering it,
200
+ * so a stale value would sit in the form as an empty-looking Select that fails
201
+ * validation on Save — clear it instead, and let the user re-pick from what
202
+ * the new day actually has. A day that keeps the slot keeps the value.
203
+ *
204
+ * Only for a day that is itself selectable: on a PAST day every slot is in the
205
+ * past, and clearing the time there would replace the real complaint ("that
206
+ * day has gone") with a second, misleading one ("pick a time").
207
+ *
208
+ * Never for DEVICE_LOCAL — no slot of a selectable day is withheld there, so
209
+ * moving the day cannot invalidate the time (`isScheduleStartInPast` answers
210
+ * false, which is what makes this a no-op rather than a second condition).
211
+ */
212
+ const handleDateChange = useCallback(
213
+ (onChange: (date: Date | null) => void, date: Date | null) => {
214
+ onChange(date);
215
+ const time = getValues('scheduledTime');
216
+ if (date && time && date >= minDate && isScheduleStartInPast(date, time, getValues('timeReference')))
217
+ setValue('scheduledTime', '');
218
+ },
219
+ [getValues, setValue, minDate],
220
+ );
221
+
222
+ /**
223
+ * Switching the reading can invalidate the time already picked: the two do not
224
+ * share a slot grid in the 45-minute zones (see `getTimeSlotOptions`), so a
225
+ * value the dropdown no longer lists would sit in the form as an empty-looking
226
+ * Select that fails on Save. Cleared for the same reason a day change clears
227
+ * it — and asked of the GRID, not of the option list, so a stored slot that
228
+ * has merely gone by is left alone.
229
+ *
230
+ * Nothing else is reset. Recurrence and the offline rule survive the switch:
231
+ * "every day, on each device's own clock" is a setting the user meant, not a
232
+ * leftover, and the API refusing it today is a message on Save rather than a
233
+ * reason to silently undo a choice.
234
+ */
235
+ const handleTimeReferenceChange = useCallback(
236
+ (onChange: (next: ScheduleTimeReference) => void, next: ScheduleTimeReference) => {
237
+ onChange(next);
238
+ const time = getValues('scheduledTime');
239
+ if (time && !isSlotOnGrid(time, next)) setValue('scheduledTime', '');
240
+ },
241
+ [getValues, setValue],
242
+ );
243
+
244
+ // Shown IMMEDIATELY, unlike every other rule on this form, which waits for
245
+ // Save: the calendar cannot withhold a past day itself (see `minDate`), so a
246
+ // picked one has to say so at once rather than look accepted until Save
247
+ // refuses it. Same predicate the schema uses — stored past starts exempt — so
248
+ // the field and the save can never disagree.
249
+ const startAtStored = useWatch({ control, name: 'startAtStored' });
250
+ const startsInPast =
251
+ !eventDriven && isStartInPastAndChanged(scheduledDate, scheduledTime, startAtStored, timeReference);
252
+ // On the field the user can act on: a past DAY is the date's problem, a past
253
+ // slot of today is the time's.
254
+ const pastDateError = startsInPast && scheduledDate && scheduledDate < minDate ? PAST_START_MESSAGE : undefined;
255
+ const pastTimeError = startsInPast && !pastDateError ? PAST_START_MESSAGE : undefined;
256
+
257
+ /**
258
+ * Today, picked after its last slot has gone by: `timeSlots` is empty and the
259
+ * Time dropdown has nothing to give. Reported on the DATE — that is the field
260
+ * the user has to move — and shown immediately, for the same reason
261
+ * `pastDateError` is: the calendar accepted the day, so nothing else says it
262
+ * cannot work until Save refuses it for a missing time.
263
+ *
264
+ * `timeSlots` is only ever empty in this exact case. A future day keeps all 48
265
+ * slots, a PAST day keeps them too, and a stored slot that has gone by is
266
+ * re-added above — so an empty list means today, and no way to finish.
267
+ */
268
+ const noSlotsLeftToday = !eventDriven && timeSlots.length === 0;
269
+ const dateError = pastDateError ?? (noSlotsLeftToday ? NO_SLOTS_TODAY_MESSAGE : undefined);
270
+
271
+ return (
272
+ <div
273
+ inert={eventDriven}
274
+ style={{
275
+ gridTemplateRows: eventDriven ? '0fr' : '1fr',
276
+ opacity: eventDriven ? 0 : 1,
277
+ }}
278
+ className="mb-[calc(-1*var(--spacing-system-lf))] grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none"
279
+ >
280
+ <div className="min-h-0 overflow-hidden">
281
+ {/* Four equal columns, as the design lays them out. A grid rather than a
282
+ flex row because the Timezone picker makes it five cells: the wrap is
283
+ then the one `col-start-1` below, and both layouts are the same
284
+ markup. */}
285
+ <div className="grid grid-cols-1 gap-[var(--spacing-system-lf)] pb-[var(--spacing-system-lf)] content-md:grid-cols-4 content-md:items-end">
286
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
287
+ <Label className="text-h4">Date</Label>
288
+ <Controller
289
+ name="scheduledDate"
290
+ control={control}
291
+ render={({ field, fieldState }) => (
292
+ // The core picker has no clear affordance, so the design's xmark
293
+ // rides on top of the trigger's free right edge.
294
+ <div className="relative">
295
+ <DatePickerInputSimple
296
+ placeholder="Select date"
297
+ value={field.value ?? undefined}
298
+ onChange={date => handleDateChange(field.onChange, date ?? null)}
299
+ // No day before today: a schedule cannot start in the past.
300
+ fromDate={minDate}
301
+ disabled={fieldsDisabled}
302
+ className="w-full"
303
+ error={dateError ?? (showErrors ? fieldState.error?.message : undefined)}
304
+ invalid={!!dateError || (showErrors && !!fieldState.error)}
305
+ />
306
+ {field.value && !fieldsDisabled && (
307
+ <button
308
+ type="button"
309
+ onClick={() => field.onChange(null)}
310
+ aria-label="Clear date"
311
+ className="absolute right-3 top-6 -translate-y-1/2 text-ods-text-secondary hover:text-ods-text-primary"
312
+ >
313
+ <XmarkCircleIcon size={24} />
314
+ </button>
315
+ )}
316
+ </div>
317
+ )}
318
+ />
319
+ </div>
320
+
321
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
322
+ <Label className="text-h4">Time</Label>
323
+ <Controller
324
+ name="scheduledTime"
325
+ control={control}
326
+ render={({ field, fieldState }) => (
327
+ // `''` reads as "no selection" to Radix, so the placeholder shows
328
+ // until a slot is picked — a date at midnight no longer
329
+ // masquerades as a chosen 12:00 AM.
330
+ <Select value={field.value} onValueChange={field.onChange} disabled={fieldsDisabled}>
331
+ {/* No `border-ods-border` here: the trigger already sets it, and
332
+ a repeat of it in `className` lands AFTER the invalid branch
333
+ in the component's `cn()`, so tailwind-merge would drop the
334
+ error border. */}
335
+ <SelectTrigger
336
+ className="w-full"
337
+ error={pastTimeError ?? (showErrors ? fieldState.error?.message : undefined)}
338
+ invalid={!!pastTimeError || (showErrors && !!fieldState.error)}
339
+ >
340
+ <SelectValue placeholder="Select time" />
341
+ </SelectTrigger>
342
+ <SelectContent>
343
+ {timeSlots.length === 0 ? (
344
+ // Deliberately NOT a `SelectItem` — there is nothing here
345
+ // to select, and a selectable row would put a bogus value
346
+ // in the form. It replaces a popup that opened blank; the
347
+ // Date field carries the same sentence for whoever never
348
+ // opens this one.
349
+ <p className="px-4 py-[var(--spacing-system-sf)] text-ods-text-secondary text-h6">
350
+ {NO_SLOTS_TODAY_MESSAGE}
351
+ </p>
352
+ ) : (
353
+ timeSlots.map(opt => (
354
+ <SelectItem key={opt.value} value={opt.value}>
355
+ {opt.label}
356
+ </SelectItem>
357
+ ))
358
+ )}
359
+ </SelectContent>
360
+ </Select>
361
+ )}
362
+ />
363
+ </div>
364
+
365
+ {showTimeReference && (
366
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
367
+ <Label className="text-h4">Timezone</Label>
368
+ <Controller
369
+ name="timeReference"
370
+ control={control}
371
+ render={({ field }) => (
372
+ <Select
373
+ value={field.value}
374
+ onValueChange={(next: ScheduleTimeReference) => handleTimeReferenceChange(field.onChange, next)}
375
+ disabled={fieldsDisabled}
376
+ >
377
+ {/* No placeholder: the field always holds one of the two
378
+ readings — SERVER is the default a schedule carries when
379
+ nothing was picked, not an empty state. */}
380
+ <SelectTrigger className="w-full" aria-label="Timezone">
381
+ <SelectValue />
382
+ </SelectTrigger>
383
+ <SelectContent>
384
+ {SCHEDULE_TIME_REFERENCE_OPTIONS.map(opt => (
385
+ <SelectItem key={opt.value} value={opt.value}>
386
+ {opt.label}
387
+ </SelectItem>
388
+ ))}
389
+ </SelectContent>
390
+ </Select>
391
+ )}
392
+ />
393
+ </div>
394
+ )}
395
+
396
+ {/* Back to column 1 when the Timezone picker took the third: the
397
+ design wraps Repeat onto its own line rather than squeezing five
398
+ controls into four columns. */}
399
+ <div className={cn('min-w-0', showTimeReference && 'content-md:col-start-1')}>
400
+ <Controller
401
+ name="repeatEnabled"
402
+ control={control}
403
+ render={({ field }) => (
404
+ <CheckboxBlock
405
+ label="Repeat Script Run"
406
+ checked={field.value}
407
+ onCheckedChange={next => {
408
+ field.onChange(next);
409
+ // Turning recurrence off removes the cadence the window is
410
+ // measured against, so the rule stops applying entirely.
411
+ recheckReconnectWindow();
412
+ }}
413
+ disabled={fieldsDisabled}
414
+ className="w-full"
415
+ />
416
+ )}
417
+ />
418
+ </div>
419
+
420
+ <div className="flex min-w-0 items-end gap-[var(--spacing-system-xs)]">
421
+ <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
422
+ <Label className="text-h4">Repeat in</Label>
423
+ <Controller
424
+ name="repeatInterval"
425
+ control={control}
426
+ render={({ field, fieldState }) => (
427
+ <ScheduleIntervalInput
428
+ min={intervalStep}
429
+ className="w-full"
430
+ value={field.value}
431
+ onChange={field.onChange}
432
+ onBlur={() => {
433
+ field.onBlur();
434
+ recheckReconnectWindow();
435
+ }}
436
+ disabled={fieldsDisabled || !repeatEnabled}
437
+ error={showErrors ? fieldState.error?.message : undefined}
438
+ invalid={showErrors && !!fieldState.error}
439
+ />
440
+ )}
441
+ />
442
+ </div>
443
+ <div className="min-w-0 flex-1">
444
+ <Controller
445
+ name="repeatUnit"
446
+ control={control}
447
+ render={({ field }) => (
448
+ <Select
449
+ value={field.value}
450
+ // Switching TO minutes drags the interval up onto the grid
451
+ // with it: "1 Day" would otherwise become "1 Minute", a
452
+ // value the user never typed and the form cannot save. The
453
+ // other direction needs nothing — every coarser unit is
454
+ // already a whole number of slots at any interval.
455
+ onValueChange={(next: DurationUnit) => {
456
+ field.onChange(next);
457
+ const snapped = snapRepeatInterval(getValues('repeatInterval'), next);
458
+ if (snapped !== getValues('repeatInterval')) setValue('repeatInterval', snapped);
459
+ recheckReconnectWindow();
460
+ }}
461
+ disabled={fieldsDisabled || !repeatEnabled}
462
+ >
463
+ <SelectTrigger className="w-full">
464
+ <SelectValue />
465
+ </SelectTrigger>
466
+ <SelectContent>
467
+ {DURATION_UNIT_OPTIONS.map(opt => (
468
+ <SelectItem key={opt.value} value={opt.value}>
469
+ {opt.label}
470
+ </SelectItem>
471
+ ))}
472
+ </SelectContent>
473
+ </Select>
474
+ )}
475
+ />
476
+ </div>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ );
482
+ }