@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,121 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { Input } from '../../../ui/input';
5
+
6
+ interface ScheduleIntervalInputProps {
7
+ /** The form's value. `null` = the box is empty. */
8
+ value: number | null;
9
+ onChange: (next: number | null) => void;
10
+ /**
11
+ * The form's own blur handler. Load-bearing, not optional plumbing: react-hook-form
12
+ * learns a field was left ONLY through this, and `reValidateMode: 'onBlur'` is
13
+ * what re-runs the schema. Without it an error the user has already fixed sits
14
+ * on screen until the next Save.
15
+ */
16
+ onBlur?: () => void;
17
+ disabled?: boolean;
18
+ invalid?: boolean;
19
+ error?: string;
20
+ min?: number;
21
+ className?: string;
22
+ 'aria-label'?: string;
23
+ /**
24
+ * Marks this box as where `scrollToFirstInvalidField` should put focus when
25
+ * the error is carried by an ancestor rather than by the input itself.
26
+ */
27
+ 'data-invalid-focus'?: boolean;
28
+ }
29
+
30
+ /**
31
+ * The `[interval]` half of the form's two duration pairs (Repeat in, Stop Retry
32
+ * after).
33
+ *
34
+ * **What the user types owns the display; the form value never writes back into
35
+ * it while the field has focus.** That inversion is the whole component. Binding
36
+ * a number input straight to validated state reads as a bug rather than a rule:
37
+ * the previous version coerced an emptied box back to a floor on every
38
+ * keystroke, so the digit reappeared as fast as it was deleted and "1" could not
39
+ * be typed over. Any round trip through `Number()` has the same shape in
40
+ * smaller ways — a leading zero vanishes mid-word, "10" briefly becomes "1"
41
+ * while a character is being removed.
42
+ *
43
+ * So the raw string is local state, and the form is told the PARSED value as it
44
+ * changes. Keeping the form current (rather than committing on blur alone)
45
+ * matters for one case: submitting with Enter never fires a blur, and a form
46
+ * reading a stale interval would save a number the user cannot see.
47
+ *
48
+ * The reverse direction — form → box — is deliberately narrow. It runs only
49
+ * when the field is NOT focused, which is exactly the set of changes that come
50
+ * from somewhere else: the record arriving (`useSeedForm`), or the unit
51
+ * dropdown raising the interval to its floor. While the box has focus those
52
+ * cannot happen, and nothing else may repaint what is being typed.
53
+ *
54
+ * Validation timing is the form's business, not this component's: errors are
55
+ * held until the first Save (`showErrors`) and re-checked on blur
56
+ * (`reValidateMode`), so nothing here is judged mid-word.
57
+ */
58
+ export function ScheduleIntervalInput({
59
+ value,
60
+ onChange,
61
+ onBlur,
62
+ disabled,
63
+ invalid,
64
+ error,
65
+ min,
66
+ className,
67
+ 'aria-label': ariaLabel,
68
+ 'data-invalid-focus': invalidFocus,
69
+ }: ScheduleIntervalInputProps) {
70
+ const [text, setText] = useState(() => (value === null ? '' : String(value)));
71
+ const focused = useRef(false);
72
+
73
+ // Adopts the FORM's value, never its own output — hence the functional
74
+ // `setText`, which keeps `text` out of the dependency list. Reacting to the
75
+ // text this effect just wrote is what would fight the user.
76
+ useEffect(() => {
77
+ if (focused.current) return;
78
+ const next = value === null ? '' : String(value);
79
+ setText(current => (current === next ? current : next));
80
+ }, [value]);
81
+
82
+ return (
83
+ <Input
84
+ // Left as a number field so the stepper and the numeric keypad stay. The
85
+ // one cost is that a browser reports `''` for a half-typed exponent
86
+ // ("1e"), which reads here as a cleared box — it resolves itself on blur,
87
+ // and it is the browser's own behaviour for the type rather than
88
+ // something this component can improve on.
89
+ type="number"
90
+ min={min}
91
+ inputMode="numeric"
92
+ aria-label={ariaLabel}
93
+ data-invalid-focus={invalidFocus || undefined}
94
+ className={className}
95
+ value={text}
96
+ onFocus={() => {
97
+ focused.current = true;
98
+ }}
99
+ onBlur={() => {
100
+ focused.current = false;
101
+ // Normalise what is shown to what was actually stored: "007" settles to
102
+ // "7". Only on the way out, never mid-word.
103
+ setText(value === null ? '' : String(value));
104
+ onBlur?.();
105
+ }}
106
+ onChange={e => {
107
+ const raw = e.target.value;
108
+ setText(raw);
109
+ if (raw.trim() === '') {
110
+ onChange(null);
111
+ return;
112
+ }
113
+ const parsed = Number(raw);
114
+ onChange(Number.isFinite(parsed) ? parsed : null);
115
+ }}
116
+ disabled={disabled}
117
+ invalid={invalid}
118
+ error={error}
119
+ />
120
+ );
121
+ }
@@ -0,0 +1,218 @@
1
+ 'use client';
2
+ 'use no memo';
3
+
4
+ import { useController, useFormContext, useWatch } from 'react-hook-form';
5
+ import { cn } from '../../../../utils/cn';
6
+ import { Label } from '../../../ui/label';
7
+ import { RadioGroupBlock } from '../../../ui/radio-group';
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select';
9
+ import { TruncateText } from '../../../ui/truncate-text';
10
+ import { type ScheduleFormData, SCHEDULE_OFFLINE_BEHAVIOR_OPTIONS } from './schedule-form-types';
11
+ import { ScheduleIntervalInput } from './schedule-interval-input';
12
+ import {
13
+ DURATION_UNIT_OPTIONS,
14
+ type DurationUnit,
15
+ isEventTrigger,
16
+ isRetryOnReconnect,
17
+ MIN_RECONNECT_MINUTES,
18
+ snapReconnectInterval,
19
+ } from './schedule-timing';
20
+
21
+ interface ReconnectWindowFieldsProps {
22
+ interval: number | null;
23
+ unit: DurationUnit;
24
+ onIntervalChange: (next: number | null) => void;
25
+ onIntervalBlur: () => void;
26
+ onUnitChange: (next: DurationUnit) => void;
27
+ disabled: boolean;
28
+ invalid: boolean;
29
+ /**
30
+ * Let the two controls share the width they are given instead of taking the
31
+ * design's fixed 120px each — what the full-width row below `md` needs, where
32
+ * the fixed pair plus its caption overflows a phone.
33
+ */
34
+ fluid?: boolean;
35
+ className?: string;
36
+ }
37
+
38
+ /**
39
+ * "Stop Retry after [1] [Week]" — how long a queued run stays worth running.
40
+ *
41
+ * Presentational and stateless: the two form fields are registered ONCE by the
42
+ * block below and handed down, because this renders TWICE (see the two call
43
+ * sites). Two `Controller`s on one name would also work, but one registration
44
+ * with two views is the honest description of what this is.
45
+ */
46
+ function ReconnectWindowFields({
47
+ interval,
48
+ unit,
49
+ onIntervalChange,
50
+ onIntervalBlur,
51
+ onUnitChange,
52
+ disabled,
53
+ invalid,
54
+ fluid = false,
55
+ className,
56
+ }: ReconnectWindowFieldsProps) {
57
+ const controlWidth = fluid ? 'flex-1 min-w-0' : 'w-[120px]';
58
+ return (
59
+ <div className={cn('flex items-center gap-[var(--spacing-system-xsf)]', className)}>
60
+ <span className={cn('shrink-0 text-h4', disabled ? 'text-ods-text-disabled' : 'text-ods-text-secondary')}>
61
+ Stop Retry after
62
+ </span>
63
+ <ScheduleIntervalInput
64
+ min={unit === 'minute' ? MIN_RECONNECT_MINUTES : 1}
65
+ aria-label="Stop retry after"
66
+ className={controlWidth}
67
+ value={interval}
68
+ onChange={onIntervalChange}
69
+ onBlur={onIntervalBlur}
70
+ // Where a failed Save should land. Without it the section marker below
71
+ // hands focus to its first focusable descendant, which is the "Skip
72
+ // this Run" radio — three controls away from the value to fix.
73
+ data-invalid-focus
74
+ disabled={disabled}
75
+ invalid={invalid}
76
+ />
77
+ {/* Switching to minutes drags the interval up to the floor with it: "1 Day"
78
+ would otherwise read "1 Minute", a value the user never typed and the
79
+ form cannot save. Coarser units clear the floor by construction. */}
80
+ <Select value={unit} onValueChange={(next: DurationUnit) => onUnitChange(next)} disabled={disabled}>
81
+ <SelectTrigger className={controlWidth} aria-label="Stop retry after unit">
82
+ <SelectValue />
83
+ </SelectTrigger>
84
+ <SelectContent>
85
+ {DURATION_UNIT_OPTIONS.map(opt => (
86
+ <SelectItem key={opt.value} value={opt.value}>
87
+ {opt.label}
88
+ </SelectItem>
89
+ ))}
90
+ </SelectContent>
91
+ </Select>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * "If device is offline at scheduled time" — Skip this Run vs Run when device
98
+ * comes back online, plus the reconnect window the second one needs
99
+ * (design node 460:63425).
100
+ *
101
+ * Maps to `offlineBehavior` (`SKIP` | `RETRY_ON_RECONNECT`) and
102
+ * `reconnectWindowSeconds`. The window is submitted ONLY alongside
103
+ * RETRY_ON_RECONNECT — the schema documents it as "set only when offlineBehavior
104
+ * is RETRY_ON_RECONNECT" — which is also why it is required rather than
105
+ * optional-meaning-forever: nothing in the contract says a null window queues a
106
+ * run indefinitely, so the form never offers a reading it cannot back up.
107
+ *
108
+ * The block presupposes a scheduled time the device can be absent FOR, so it
109
+ * collapses for DEVICE_ONLINE — which fires on the reconnect already — and
110
+ * submits the SKIP default instead of what the collapsed controls hold.
111
+ * DEVICE_LOCAL keeps it: its scheduled moment is the device's own wall clock.
112
+ *
113
+ * It collapses with the `0fr → 1fr` grid-rows technique, staying MOUNTED so
114
+ * toggling the trigger back restores what was picked, `inert` to drop the
115
+ * collapsed controls out of the tab order, and a padding/negative-margin pair:
116
+ * the bottom padding is the room the absolutely-positioned error renders into,
117
+ * and the constant negative margin cancels it so a collapsed block leaves no
118
+ * hole.
119
+ */
120
+ export function ScheduleOfflineFields({ showErrors, disabled = false }: { showErrors: boolean; disabled?: boolean }) {
121
+ const { control } = useFormContext<ScheduleFormData>();
122
+ const trigger = useWatch({ control, name: 'trigger' });
123
+ // The one reading with no offline moment to decide about — see the docstring.
124
+ const collapsed = isEventTrigger(trigger);
125
+
126
+ const { field: behaviorField } = useController({ control, name: 'offlineBehavior' });
127
+ const { field: intervalField, fieldState: intervalState } = useController({ control, name: 'reconnectInterval' });
128
+ const { field: unitField } = useController({ control, name: 'reconnectUnit' });
129
+
130
+ // The window belongs to the second option and only to it. Locked under SKIP
131
+ // rather than hidden: the design keeps it on the row so the choice reads as
132
+ // "skip, or retry for THIS long", and a click anywhere on that row selects the
133
+ // option and unlocks it — the controls themselves are interactive content,
134
+ // which a label's activation behavior skips, so using them never re-triggers
135
+ // the radio they sit inside.
136
+ const windowDisabled = disabled || !isRetryOnReconnect(behaviorField.value);
137
+ const intervalError = showErrors ? intervalState.error?.message : undefined;
138
+
139
+ // Switching TO minutes drags the interval up to the floor with it, the way the
140
+ // repeat pair does: "1 Day" would otherwise read "1 Minute", a value the user
141
+ // never typed and the form cannot save. A cleared field stays cleared.
142
+ const handleUnitChange = (next: DurationUnit) => {
143
+ unitField.onChange(next);
144
+ const snapped = snapReconnectInterval(intervalField.value, next);
145
+ if (snapped !== intervalField.value) intervalField.onChange(snapped);
146
+ };
147
+
148
+ const windowFieldsFor = (fluid: boolean) => (
149
+ <ReconnectWindowFields
150
+ interval={intervalField.value}
151
+ unit={unitField.value}
152
+ onIntervalChange={intervalField.onChange}
153
+ onIntervalBlur={intervalField.onBlur}
154
+ onUnitChange={handleUnitChange}
155
+ disabled={windowDisabled}
156
+ invalid={!!intervalError}
157
+ fluid={fluid}
158
+ />
159
+ );
160
+
161
+ const options = SCHEDULE_OFFLINE_BEHAVIOR_OPTIONS.map(option =>
162
+ isRetryOnReconnect(option.value)
163
+ ? // Desktop only. Below `md` the caption plus two controls cannot share a
164
+ // row with the option's own label without squeezing it to nothing, so
165
+ // the same fields render full-width under the group instead.
166
+ { ...option, trailing: <div className="hidden content-md:flex">{windowFieldsFor(false)}</div> }
167
+ : option,
168
+ );
169
+
170
+ return (
171
+ <div
172
+ inert={collapsed}
173
+ style={{
174
+ gridTemplateRows: collapsed ? '0fr' : '1fr',
175
+ opacity: collapsed ? 0 : 1,
176
+ }}
177
+ className="mb-[calc(-1*var(--spacing-system-lf))] grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none"
178
+ >
179
+ <div className="min-h-0 overflow-hidden">
180
+ {/* The bottom padding is the room the error below renders into, and it
181
+ sits on a wrapper INSIDE the clipping box rather than on the box
182
+ itself: a box is never shorter than its own padding — border-box
183
+ included — so padding on the grid item would survive the 0fr track as
184
+ 24px of residue that the negative margin above then spends cancelling
185
+ instead of closing the section gap. */}
186
+ <div className="pb-[var(--spacing-system-lf)]">
187
+ {/* `data-invalid` on the SECTION, not on the number inputs: one of
188
+ those two copies is always display:none, and an ancestor is matched
189
+ before either, so the marker is visible at every breakpoint. What
190
+ it cannot say on its own is WHERE to put focus — the first
191
+ focusable inside it is the "Skip this Run" radio — so the input
192
+ carries `data-invalid-focus` and the helper prefers it. */}
193
+ <div
194
+ className="relative flex flex-col gap-[var(--spacing-system-xxs)]"
195
+ data-invalid={intervalError ? true : undefined}
196
+ >
197
+ <Label className="text-h4">If device is offline at scheduled time</Label>
198
+ <RadioGroupBlock
199
+ variant="grouped"
200
+ value={behaviorField.value}
201
+ onValueChange={behaviorField.onChange}
202
+ disabled={disabled}
203
+ options={options}
204
+ />
205
+ <div className="pt-[var(--spacing-system-xs)] content-md:hidden">{windowFieldsFor(true)}</div>
206
+ {intervalError && (
207
+ <div className="absolute bottom-0 left-0 right-0 translate-y-full">
208
+ <TruncateText variant="h6" className="text-ods-error">
209
+ {intervalError}
210
+ </TruncateText>
211
+ </div>
212
+ )}
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ );
218
+ }
@@ -0,0 +1,321 @@
1
+ 'use client';
2
+ 'use no memo';
3
+
4
+ import { Controller, useFormContext } from 'react-hook-form';
5
+ import { useContentMdUp } from '../../../../hooks/ui/use-content-breakpoint';
6
+ import { DraggerIcon } from '../../../icons-v2-generated/interface/dragger-icon';
7
+ import { TrashIcon } from '../../../icons-v2-generated/interface/trash-icon';
8
+ import { ScriptArguments } from '../../../platform/ScriptArguments';
9
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../../../ui/accordion';
10
+ import { Button } from '../../../ui/button';
11
+ import { Input } from '../../../ui/input';
12
+ import { Label } from '../../../ui/label';
13
+ import { SortableMoveButtons } from '../../sortable-list/sortable-move-buttons';
14
+ import { useSortableItem } from '../../sortable-list/use-sortable-item';
15
+ import {
16
+ EMPTY_SCHEDULE_SCRIPT_ROW,
17
+ type RenderScheduleScriptSearch,
18
+ type ScheduleFormData,
19
+ type ScheduleFormScriptRow,
20
+ } from './schedule-form-types';
21
+
22
+ /** Sentinel for "no script picked yet" — the field is locked and renders empty. */
23
+ const UNSET_TIMEOUT = 0;
24
+
25
+ interface ScheduleScriptPickerCardProps {
26
+ index: number;
27
+ /** How many cards the list has — disables the move buttons at the ends. */
28
+ count: number;
29
+ /** UI platform ids the schedule targets — what the host narrows its script search by. */
30
+ supportedPlatforms: string[];
31
+ /** The card's "Select Script" field: the host's search over its script library. */
32
+ renderScriptSearch: RenderScheduleScriptSearch;
33
+ onRemove: () => void;
34
+ canRemove: boolean;
35
+ disabled?: boolean;
36
+ /**
37
+ * Controls inline error visibility — the parent flips this true once the
38
+ * user attempts Save, so errors stay hidden on a pristine form (mirrors
39
+ * `ScriptFormFields.showErrors`).
40
+ */
41
+ showErrors?: boolean;
42
+ /**
43
+ * The script's arguments and environment variables under the first row.
44
+ * Default true; false is the card as one row (the script and its timeout).
45
+ */
46
+ showParams?: boolean;
47
+ }
48
+
49
+ /**
50
+ * One card of the schedule's ordered script list: drag handle, script
51
+ * autocomplete, timeout and the script's arguments / environment variables.
52
+ *
53
+ * **Order is the payload.** The card index IS the run order — the backend
54
+ * stores `scriptIds: [ID!]` in run order, so reordering rewrites that array.
55
+ *
56
+ * Sorting runs on the core library's `SortableList` (`useSortableItem`), the
57
+ * same Pragmatic stack the ticket board uses. Only the handle starts a drag,
58
+ * so text selection inside the fields keeps working, and the handle carries
59
+ * the keyboard path (Arrow Up / Arrow Down, one press one move) with
60
+ * live-region announcements wired up by the list. On touch/narrow viewports
61
+ * no drag exists at all — an up/down button pair renders beside the delete
62
+ * button instead.
63
+ *
64
+ * Arguments and env vars ARE persisted, as this schedule's per-script override
65
+ * (`scriptCustomParams`): they seed from the picked script's defaults, and only
66
+ * a half the user moves off those defaults is written back — so leaving a card
67
+ * untouched keeps the schedule following later edits to the script itself.
68
+ *
69
+ * ⚠ **Timeout is still editable but not persisted**: the override input carries
70
+ * args and env vars only. It is
71
+ * seeded from the script's own default, which is what the run actually uses, so
72
+ * the card shows the truth — it just cannot be changed per schedule yet.
73
+ */
74
+ export function ScheduleScriptPickerCard({
75
+ index,
76
+ count,
77
+ supportedPlatforms,
78
+ renderScriptSearch,
79
+ onRemove,
80
+ canRemove,
81
+ disabled = false,
82
+ showErrors = true,
83
+ showParams = true,
84
+ }: ScheduleScriptPickerCardProps) {
85
+ const { control, setValue, watch } = useFormContext<ScheduleFormData>();
86
+ const selected = watch(`scripts.${index}`);
87
+
88
+ // Timeout / arguments / env vars all belong to the picked script and are
89
+ // seeded from it, so they stay locked until there is a script to belong to.
90
+ const runParamsLocked = disabled || !selected?.scriptId;
91
+
92
+ const { itemRef, dragHandleProps, isDragging, dragAndDropEnabled } = useSortableItem();
93
+ // Mobile folds the args/env editors into accordions; `undefined` (SSR/first
94
+ // render) keeps the expanded variant and settles before first paint.
95
+ const isMdUp = useContentMdUp();
96
+
97
+ const handleScriptPick = (row: ScheduleFormScriptRow | null) => {
98
+ if (!row) {
99
+ // Back to the locked state: the run parameters belong to the script, so
100
+ // clearing the script clears them rather than leaving a stale timeout.
101
+ setValue(`scripts.${index}`, EMPTY_SCHEDULE_SCRIPT_ROW, { shouldValidate: true });
102
+ return;
103
+ }
104
+ // The row the host seeds from the script itself — that IS what the
105
+ // schedule will run with, so the card never shows blanks for a real script.
106
+ // A freshly picked script therefore starts UNCUSTOMISED: the defaults ride
107
+ // along beside the editable copy, and submit writes an override only for a
108
+ // half the user actually moved off them.
109
+ setValue(`scripts.${index}`, row, { shouldValidate: true, shouldDirty: true });
110
+ };
111
+
112
+ return (
113
+ <div
114
+ ref={itemRef}
115
+ // No inline style here — the list writes `transform` straight onto this
116
+ // node during a drag (see `SortableList`).
117
+ className={`flex flex-col gap-[var(--spacing-system-sf)] rounded-[6px] border bg-ods-bg p-[var(--spacing-system-l)] content-md:gap-[var(--spacing-system-lf)] ${
118
+ isDragging ? 'relative z-10 border-ods-accent shadow-lg' : 'border-ods-border'
119
+ }`}
120
+ >
121
+ {/* Touch (no DnD): the mock stacks the rows on every width and puts the
122
+ move buttons + delete in the Select Script row, spaced by a fixed 8px;
123
+ with DnD the approved desktop layout stays — handle beside the select,
124
+ delete beside the timeout, the two halves side by side from md. */}
125
+ <div
126
+ className={`flex flex-col gap-[var(--spacing-system-sf)] content-md:gap-[var(--spacing-system-lf)] ${dragAndDropEnabled ? 'content-md:flex-row content-md:items-end' : ''}`}
127
+ >
128
+ <div
129
+ className={`flex min-w-0 flex-1 items-end ${
130
+ dragAndDropEnabled ? 'gap-[var(--spacing-system-xs)]' : 'gap-[var(--spacing-system-xsf)]'
131
+ }`}
132
+ >
133
+ {dragAndDropEnabled && (
134
+ <button
135
+ type="button"
136
+ {...dragHandleProps}
137
+ disabled={disabled}
138
+ aria-label={`Reorder ${selected?.name || `script ${index + 1}`}`}
139
+ className={`flex size-12 shrink-0 items-center justify-center rounded-[6px] text-ods-text-secondary hover:text-ods-text-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-ods-border-focus disabled:cursor-not-allowed disabled:opacity-30 ${
140
+ isDragging ? 'cursor-grabbing text-ods-text-primary' : 'cursor-grab'
141
+ }`}
142
+ >
143
+ <DraggerIcon size={24} />
144
+ </button>
145
+ )}
146
+
147
+ <Controller
148
+ name={`scripts.${index}.scriptId`}
149
+ control={control}
150
+ render={({ fieldState }) => (
151
+ <>
152
+ {renderScriptSearch({
153
+ scriptId: selected?.scriptId ?? '',
154
+ name: selected?.name ?? '',
155
+ supportedPlatforms,
156
+ onPick: handleScriptPick,
157
+ disabled,
158
+ error: showErrors ? fieldState.error?.message : undefined,
159
+ invalid: showErrors && !!fieldState.error,
160
+ })}
161
+ </>
162
+ )}
163
+ />
164
+
165
+ {!dragAndDropEnabled && (
166
+ <>
167
+ <SortableMoveButtons index={index} count={count} label={selected?.name || `script ${index + 1}`} />
168
+ <Button
169
+ variant="outline"
170
+ size="icon"
171
+ onClick={onRemove}
172
+ disabled={!canRemove || disabled}
173
+ aria-label="Remove script from schedule"
174
+ className="text-ods-error disabled:opacity-30"
175
+ >
176
+ <TrashIcon size={20} />
177
+ </Button>
178
+ </>
179
+ )}
180
+ </div>
181
+
182
+ <div className="flex min-w-0 flex-1 items-end gap-[var(--spacing-system-xs)]">
183
+ <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
184
+ <Label className="text-h4">Timeout</Label>
185
+ <Controller
186
+ name={`scripts.${index}.timeoutSeconds`}
187
+ control={control}
188
+ render={({ field, fieldState }) => (
189
+ // Locked until a script is picked: the timeout belongs to the
190
+ // script, so the field stays EMPTY until there is one — the
191
+ // sentinel is a "nothing here yet" marker, and rendering it as
192
+ // a literal 0 would read as a real (and invalid) timeout.
193
+ // Picking a script writes its own timeout in.
194
+ <Input
195
+ type="number"
196
+ min={1}
197
+ className="w-full"
198
+ value={field.value ? String(field.value) : ''}
199
+ onChange={e => field.onChange(e.target.value ? Number(e.target.value) : UNSET_TIMEOUT)}
200
+ disabled={runParamsLocked}
201
+ endAdornment={<span className="text-ods-text-secondary text-h6">Seconds</span>}
202
+ error={showErrors ? fieldState.error?.message : undefined}
203
+ invalid={showErrors && !!fieldState.error}
204
+ />
205
+ )}
206
+ />
207
+ </div>
208
+
209
+ {dragAndDropEnabled && (
210
+ <Button
211
+ variant="outline"
212
+ size="icon"
213
+ onClick={onRemove}
214
+ disabled={!canRemove || disabled}
215
+ aria-label="Remove script from schedule"
216
+ className="text-ods-error disabled:opacity-30"
217
+ >
218
+ <TrashIcon size={20} />
219
+ </Button>
220
+ )}
221
+ </div>
222
+ </div>
223
+
224
+ {/* Arguments / env vars are the same class of field as Timeout — they
225
+ belong to the picked script and are seeded from it — so they stay
226
+ locked until there is a script to belong to.
227
+ Mobile folds the two editors into "Edit Default …" accordions (per the
228
+ mock); from md up they stay expanded side by side. The editors are the
229
+ only difference — both branches render the same controlled fields. */}
230
+ {!showParams ? null : isMdUp === false ? (
231
+ <Accordion type="multiple" className="overflow-hidden rounded-[6px] border border-ods-border">
232
+ <AccordionItem value="args" className="border-ods-border data-[state=closed]:bg-ods-card">
233
+ <AccordionTrigger className="h-14 px-[var(--spacing-system-sf)] py-0 text-ods-text-primary text-h6 hover:no-underline [&>svg]:text-ods-text-secondary">
234
+ Edit Default Script Arguments
235
+ </AccordionTrigger>
236
+ {/* The editor's own title would duplicate the trigger — an empty
237
+ titleLabel renders an empty <label>, hidden by :empty. */}
238
+ <AccordionContent className="px-[var(--spacing-system-sf)] [&_label:empty]:hidden">
239
+ <Controller
240
+ name={`scripts.${index}.args`}
241
+ control={control}
242
+ render={({ field }) => (
243
+ <ScriptArguments
244
+ arguments={field.value}
245
+ onArgumentsChange={field.onChange}
246
+ keyPlaceholder="Key"
247
+ valuePlaceholder="Enter Value (empty=flag)"
248
+ addButtonLabel="Add Script Argument"
249
+ titleLabel=""
250
+ disabled={runParamsLocked}
251
+ />
252
+ )}
253
+ />
254
+ </AccordionContent>
255
+ </AccordionItem>
256
+ <AccordionItem value="env" className="border-b-0 border-ods-border data-[state=closed]:bg-ods-card">
257
+ <AccordionTrigger className="h-14 px-[var(--spacing-system-sf)] py-0 text-ods-text-primary text-h6 hover:no-underline [&>svg]:text-ods-text-secondary">
258
+ Edit Default Environment Vars
259
+ </AccordionTrigger>
260
+ <AccordionContent className="px-[var(--spacing-system-sf)] [&_label:empty]:hidden">
261
+ <Controller
262
+ name={`scripts.${index}.envVars`}
263
+ control={control}
264
+ render={({ field }) => (
265
+ <ScriptArguments
266
+ arguments={field.value}
267
+ onArgumentsChange={field.onChange}
268
+ keyPlaceholder="Key"
269
+ valuePlaceholder="Enter Value"
270
+ addButtonLabel="Add Environment Var"
271
+ addSecretButtonLabel="Add Secret"
272
+ titleLabel=""
273
+ disabled={runParamsLocked}
274
+ />
275
+ )}
276
+ />
277
+ </AccordionContent>
278
+ </AccordionItem>
279
+ </Accordion>
280
+ ) : (
281
+ <div className="flex flex-col items-start gap-[var(--spacing-system-lf)] content-md:flex-row">
282
+ <div className="w-full min-w-0 flex-1 [&_label]:text-h4">
283
+ <Controller
284
+ name={`scripts.${index}.args`}
285
+ control={control}
286
+ render={({ field }) => (
287
+ <ScriptArguments
288
+ arguments={field.value}
289
+ onArgumentsChange={field.onChange}
290
+ keyPlaceholder="Key"
291
+ valuePlaceholder="Enter Value (empty=flag)"
292
+ addButtonLabel="Add Script Argument"
293
+ titleLabel="Script Arguments"
294
+ disabled={runParamsLocked}
295
+ />
296
+ )}
297
+ />
298
+ </div>
299
+ <div className="w-full min-w-0 flex-1 [&_label]:text-h4">
300
+ <Controller
301
+ name={`scripts.${index}.envVars`}
302
+ control={control}
303
+ render={({ field }) => (
304
+ <ScriptArguments
305
+ arguments={field.value}
306
+ onArgumentsChange={field.onChange}
307
+ keyPlaceholder="Key"
308
+ valuePlaceholder="Enter Value"
309
+ addButtonLabel="Add Environment Var"
310
+ addSecretButtonLabel="Add Secret"
311
+ titleLabel="Environment Vars"
312
+ disabled={runParamsLocked}
313
+ />
314
+ )}
315
+ />
316
+ </div>
317
+ </div>
318
+ )}
319
+ </div>
320
+ );
321
+ }