@colixsystems/widget-sdk 0.234.0 → 0.236.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.
@@ -0,0 +1,221 @@
1
+ // REQ-WSDK-PLATFORM §6 — the ONE `<DateTimePicker>` field both hosts render.
2
+ // The field (icon, value in the viewer's language, clear button) is shared;
3
+ // what opens when it is pressed is the host's `Surface`: a themed calendar
4
+ // sheet on the web, the operating system's own picker on a device.
5
+
6
+ import React from "react";
7
+ import { useHostLocale, useHostTheme } from "./hooks.js";
8
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
9
+ import { Icon } from "./icon.js";
10
+ import { FIELD_OUTLINE_OFF, fieldStateStyle, resolveFormControlTokens } from "./form-control-tokens.js";
11
+ import {
12
+ formatDisplayLabel,
13
+ normaliseMode,
14
+ partsToIso,
15
+ valueToParts,
16
+ } from "./datetimepicker-format.js";
17
+ import { resolveBounds, resolvePickerLocale } from "./datetimepicker-calendar.js";
18
+
19
+ const h = React.createElement;
20
+ const CLEAR_ICON_SIZE = 18;
21
+
22
+ function isSet(value) {
23
+ return value != null && value !== "";
24
+ }
25
+
26
+ /**
27
+ * Bind the shared field to one platform's primitives and picking surface.
28
+ *
29
+ * @param {{View: any, Text: any, Pressable: any, Platform: any, Surface: any}} rn
30
+ */
31
+ export function makeDateTimePicker(rn) {
32
+ const { View, Text, Pressable, Platform, Surface } = rn;
33
+
34
+ function focusNode(node) {
35
+ if (Platform.OS === "web" && node && typeof node.focus === "function") node.focus();
36
+ }
37
+
38
+ function ClearButton({ t, label, onPress, testID }) {
39
+ return h(
40
+ Pressable,
41
+ {
42
+ onPress,
43
+ accessibilityRole: "button",
44
+ accessibilityLabel: label,
45
+ hitSlop: 8,
46
+ testID: testID ? `${testID}-clear` : undefined,
47
+ style: ({ pressed }) => [
48
+ {
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ alignSelf: "stretch",
52
+ paddingHorizontal: t.paddingHorizontal,
53
+ },
54
+ FIELD_OUTLINE_OFF,
55
+ pressed ? { opacity: 0.6 } : null,
56
+ ],
57
+ },
58
+ h(Icon, { name: "x", size: CLEAR_ICON_SIZE, color: t.placeholderColor }),
59
+ );
60
+ }
61
+
62
+ function Trigger({ t, triggerRef, open, canEdit, hasValue, shown, mode, spoken, onOpen, onFocusChange, testID }) {
63
+ const ink = hasValue ? t.textColor : t.placeholderColor;
64
+ return h(
65
+ Pressable,
66
+ {
67
+ ref: triggerRef,
68
+ onPress: () => {
69
+ if (canEdit) onOpen();
70
+ },
71
+ onKeyDown: (event) => {
72
+ if (canEdit && event.key === "ArrowDown") {
73
+ event.preventDefault();
74
+ onOpen();
75
+ }
76
+ },
77
+ onFocus: () => onFocusChange(true),
78
+ onBlur: () => onFocusChange(false),
79
+ disabled: !canEdit,
80
+ accessibilityRole: "button",
81
+ "aria-haspopup": "dialog",
82
+ "aria-expanded": open,
83
+ "aria-disabled": !canEdit,
84
+ accessibilityLabel: spoken,
85
+ testID,
86
+ style: [
87
+ {
88
+ flex: 1,
89
+ flexDirection: "row",
90
+ alignItems: "center",
91
+ gap: t.rowGap,
92
+ minHeight: t.height,
93
+ paddingLeft: t.paddingHorizontal,
94
+ paddingRight: hasValue && canEdit ? 0 : t.paddingHorizontal,
95
+ paddingVertical: t.paddingVertical,
96
+ },
97
+ FIELD_OUTLINE_OFF,
98
+ ],
99
+ },
100
+ h(Icon, { name: mode === "time" ? "clock" : "calendar", size: CLEAR_ICON_SIZE, color: ink }),
101
+ h(
102
+ Text,
103
+ { numberOfLines: 1, style: { flexShrink: 1, color: ink, fontFamily: t.fontFamily, fontSize: t.fontSize } },
104
+ shown,
105
+ ),
106
+ );
107
+ }
108
+
109
+ function DateTimePicker({
110
+ value,
111
+ onChange,
112
+ mode,
113
+ minimumDate,
114
+ maximumDate,
115
+ disabled = false,
116
+ accessibilityLabel,
117
+ placeholder,
118
+ clearLabel = "Clear",
119
+ doneLabel = "Done",
120
+ closeAccessibilityLabel,
121
+ testID,
122
+ }) {
123
+ const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
124
+ const t = resolveFormControlTokens(theme);
125
+ const appLocale = useHostLocale();
126
+ const locale = React.useMemo(() => resolvePickerLocale(appLocale), [appLocale]);
127
+ const effectiveMode = normaliseMode(mode);
128
+ const [open, setOpen] = React.useState(false);
129
+ const [focused, setFocused] = React.useState(false);
130
+ const triggerRef = React.useRef(null);
131
+ const parts = React.useMemo(() => valueToParts(value, effectiveMode), [value, effectiveMode]);
132
+ const bounds = React.useMemo(
133
+ () => resolveBounds(minimumDate, maximumDate, effectiveMode),
134
+ [minimumDate, maximumDate, effectiveMode],
135
+ );
136
+ const hasValue = isSet(value);
137
+ const shown =
138
+ !hasValue && isSet(placeholder) ? placeholder : formatDisplayLabel(value, effectiveMode, locale);
139
+ const canEdit = !disabled && typeof onChange === "function";
140
+
141
+ const emit = (next) => {
142
+ if (typeof onChange === "function") onChange(partsToIso(next, effectiveMode));
143
+ };
144
+ // Back to the field once the sheet is gone, or its focus trap pulls it in.
145
+ const wasOpen = React.useRef(false);
146
+ React.useEffect(() => {
147
+ if (wasOpen.current && !open) focusNode(triggerRef.current);
148
+ wasOpen.current = open;
149
+ }, [open]);
150
+
151
+ const trigger = h(Trigger, {
152
+ t,
153
+ triggerRef,
154
+ open,
155
+ canEdit,
156
+ hasValue,
157
+ shown,
158
+ mode: effectiveMode,
159
+ spoken: [accessibilityLabel, shown].filter(Boolean).join(", "),
160
+ onOpen: () => setOpen(true),
161
+ onFocusChange: setFocused,
162
+ testID,
163
+ });
164
+
165
+ const field = h(
166
+ View,
167
+ {
168
+ style: [
169
+ {
170
+ flexDirection: "row",
171
+ alignItems: "center",
172
+ borderWidth: 1,
173
+ borderColor: t.borderColor,
174
+ borderRadius: t.radius,
175
+ backgroundColor: t.background,
176
+ },
177
+ fieldStateStyle(t, { focused, invalid: false }),
178
+ disabled ? { opacity: t.disabledOpacity } : null,
179
+ ],
180
+ },
181
+ trigger,
182
+ hasValue && canEdit
183
+ ? h(ClearButton, {
184
+ t,
185
+ label: clearLabel,
186
+ testID,
187
+ onPress: () => {
188
+ emit(null);
189
+ // The button unmounts once cleared; focus stays on the field.
190
+ focusNode(triggerRef.current);
191
+ },
192
+ })
193
+ : null,
194
+ );
195
+
196
+ // A sibling of the field: iOS draws its picker inline, below it.
197
+ return h(
198
+ React.Fragment,
199
+ null,
200
+ field,
201
+ h(Surface, {
202
+ open,
203
+ onClose: () => setOpen(false),
204
+ mode: effectiveMode,
205
+ parts,
206
+ bounds,
207
+ locale,
208
+ onPick: emit,
209
+ title: accessibilityLabel || shown,
210
+ heading: accessibilityLabel,
211
+ doneLabel,
212
+ closeAccessibilityLabel,
213
+ testID,
214
+ t,
215
+ }),
216
+ );
217
+ }
218
+
219
+ DateTimePicker.displayName = "DateTimePicker";
220
+ return DateTimePicker;
221
+ }
@@ -1,166 +1,15 @@
1
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (web implementation).
2
- //
3
- // `@react-native-community/datetimepicker` ships iOS / Android only — it has
4
- // no react-native-web mapping, so importing it in the browser yields no
5
- // rendered control. The web build therefore uses the browser's native
6
- // `<input type="date" | "time" | "datetime-local">` directly and keeps the
7
- // public contract identical to the native build: the value prop and the
8
- // onChange callback both speak ISO 8601 strings (the datastore wire format),
9
- // so a widget's JSX is byte-for-byte the same on both platforms.
10
- //
11
- // Props (mirrors datetimepicker.native.js):
12
- // value: string | null — ISO 8601 (`2026-05-28` for date mode,
13
- // `14:30` for time mode,
14
- // `2026-05-28T14:30:00.000Z` for datetime mode).
15
- // `null` / "" leaves the input blank.
16
- // onChange: (iso: string) => void
17
- // mode: "date" | "time" | "datetime" — default "date"
18
- // minimumDate / maximumDate: string | null — ISO bounds
19
- // disabled: boolean
20
- // accessibilityLabel: string | undefined — written to the DOM input as
21
- // `aria-label`. The shared form-field renderer
22
- // in `frontend/src/components/widgets/_shared/
23
- // formFields.jsx` passes the column's label
24
- // through this prop so Playwright / screen
25
- // readers can locate the input by its label.
1
+ // `<DateTimePicker>` (web). Binds ./datetimepicker-view.js to react-native-web
2
+ // and the themed calendar sheet; see ./primitives.js for why not the bare
3
+ // specifier.
26
4
 
27
- import React from "react";
28
- import { useHostTheme } from "./hooks.js";
29
- import {
30
- isDarkColor,
31
- isoToDateTimeLocalInput,
32
- dateTimeLocalInputToIso,
33
- } from "./datetimepicker-format.js";
34
- import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
5
+ import { View, Text, Pressable, ScrollView, Platform } from "react-native-web";
6
+ import { makeDateTimePicker } from "./datetimepicker-view.js";
7
+ import { makeDateTimeSheet } from "./datetimepicker-sheet.js";
8
+ import { Overlay } from "./overlay.js";
35
9
 
36
- const MODE_TO_INPUT_TYPE = {
37
- date: "date",
38
- time: "time",
39
- datetime: "datetime-local",
40
- };
10
+ const primitives = { View, Text, Pressable, ScrollView, Platform };
41
11
 
42
- // Render the saved ISO value in the format each `<input>` accepts.
43
- //
44
- // `date` and `time` are already local / timezone-free in the SDK contract,
45
- // so they pass through verbatim. `datetime-local` needs `YYYY-MM-DDTHH:mm`
46
- // in LOCAL time — we round-trip through Date to convert a stored UTC ISO
47
- // (e.g. `2026-05-28T13:30:00.000Z`) to the user's local clock.
48
- function _isoToInputValue(value, mode) {
49
- if (value == null || value === "") return "";
50
- if (mode === "date") {
51
- // Strip any time portion if a datetime ISO accidentally lands in a
52
- // date-mode picker — keep the YYYY-MM-DD head.
53
- return String(value).slice(0, 10);
54
- }
55
- if (mode === "time") {
56
- return String(value).slice(0, 5);
57
- }
58
- return isoToDateTimeLocalInput(value);
59
- }
60
-
61
- // Translate the input's typed string back to the ISO format the wire wants.
62
- // `date` and `time` are already in the right shape; `datetime-local` is
63
- // parsed as local time and serialized as the full UTC ISO so the value
64
- // round-trips into a DATE column unchanged.
65
- function _inputValueToIso(raw, mode) {
66
- if (raw == null || raw === "") return "";
67
- if (mode === "date" || mode === "time") return raw;
68
- return dateTimeLocalInputToIso(raw);
69
- }
70
-
71
- export function DateTimePicker({
72
- value,
73
- onChange,
74
- mode,
75
- minimumDate,
76
- maximumDate,
77
- disabled,
78
- accessibilityLabel,
79
- }) {
80
- const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
81
- const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
82
- const inputType = MODE_TO_INPUT_TYPE[effectiveMode];
83
- const inputValue = _isoToInputValue(value, effectiveMode);
84
-
85
- const handleChange = (event) => {
86
- if (typeof onChange !== "function") return;
87
- const iso = _inputValueToIso(event.target.value, effectiveMode);
88
- onChange(iso);
89
- };
90
-
91
- // The min/max props accept ISO strings too — pass them through in the
92
- // input-friendly representation. Undefined when not provided so the
93
- // browser doesn't show an empty constraint chip.
94
- const min =
95
- minimumDate != null && minimumDate !== ""
96
- ? _isoToInputValue(minimumDate, effectiveMode)
97
- : undefined;
98
- const max =
99
- maximumDate != null && maximumDate !== ""
100
- ? _isoToInputValue(maximumDate, effectiveMode)
101
- : undefined;
102
-
103
- // sc-1878 — clicking the formatted date text (or focusing it and pressing
104
- // Enter / Space) opens the native calendar, not just the small built-in
105
- // calendar icon. `showPicker()` requires a user gesture; click/keydown both
106
- // qualify. Guarded: not every browser/build exposes it, and calling it while
107
- // the picker is already open throws — neither should break the field.
108
- const openPicker = (target) => {
109
- if (disabled || !target || typeof target.showPicker !== "function") return;
110
- try {
111
- target.showPicker();
112
- } catch {
113
- // Already open, or blocked outside a user gesture — leave the input as-is.
114
- }
115
- };
116
-
117
- const handleClick = (event) => openPicker(event.currentTarget);
118
-
119
- const handleKeyDown = (event) => {
120
- if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
121
- event.preventDefault();
122
- openPicker(event.currentTarget);
123
- }
124
- };
125
-
126
- // Inline styles match the SDK's other web-only primitives — the host's
127
- // form widgets wrap this in their own labelled field, so the input just
128
- // needs to look like a normal text input. `cursor: pointer` signals the
129
- // whole field is the click affordance (sc-1878). Colours come from the
130
- // theme so the field is legible on any surface (sc-3370); `colorScheme`
131
- // steers the browser's built-in date UI (edit segments + calendar icon).
132
- // The scheme keys off the theme's TEXT colour (onSurface, which the host
133
- // already picked to contrast the surface) rather than re-deriving dark-vs-
134
- // light from the surface with a second luminance formula that could disagree
135
- // with the host: light text ⇒ dark surface ⇒ dark scheme.
136
- const style = {
137
- boxSizing: "border-box",
138
- width: "100%",
139
- minHeight: 44,
140
- padding: "8px 12px",
141
- fontSize: 16,
142
- fontFamily: "inherit",
143
- color: theme.colors.onSurface,
144
- backgroundColor: "transparent",
145
- colorScheme: isDarkColor(theme.colors.onSurface) ? "light" : "dark",
146
- borderWidth: 1,
147
- borderStyle: "solid",
148
- borderColor: theme.colors.border,
149
- borderRadius: 6,
150
- outline: "none",
151
- cursor: disabled ? "default" : "pointer",
152
- };
153
-
154
- return React.createElement("input", {
155
- type: inputType,
156
- value: inputValue,
157
- onChange: handleChange,
158
- onClick: handleClick,
159
- onKeyDown: handleKeyDown,
160
- min,
161
- max,
162
- disabled: !!disabled,
163
- style,
164
- "aria-label": accessibilityLabel || undefined,
165
- });
166
- }
12
+ export const DateTimePicker = makeDateTimePicker({
13
+ ...primitives,
14
+ Surface: makeDateTimeSheet({ ...primitives, Overlay }),
15
+ });
@@ -1,131 +1,43 @@
1
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (native).
2
- //
3
- // Wraps `@react-native-community/datetimepicker`. That library is imperative
4
- // on Android — rendering it shows the dialog immediately and re-mounting is
5
- // the only way to reopen it — so the primitive owns a small open/closed state
6
- // and renders a tappable, formatted-date trigger (sc-1878). Tapping the
7
- // formatted date (the trigger is an accessible button, reachable by keyboard /
8
- // screen-reader) opens the picker; choosing a value closes it and emits the
9
- // ISO string. This mirrors the web build, where clicking the `<input>` text
10
- // calls `showPicker()` — both hosts: tap the date, the picker opens.
11
- //
12
- // Props (mirrors datetimepicker.js):
13
- // value: string | null — ISO 8601 (`2026-05-28` for date mode,
14
- // `14:30` for time mode,
15
- // `2026-05-28T14:30:00.000Z` for datetime mode).
16
- // `null` / "" shows the placeholder, opens at "now".
17
- // onChange: (iso: string) => void
18
- // mode: "date" | "time" | "datetime" — default "date"
19
- // minimumDate / maximumDate: string | null — ISO bounds
20
- // disabled: boolean
21
- // accessibilityLabel: string | undefined — names the trigger button so the
22
- // shared form-field renderer's column label reaches
23
- // screen readers / test tooling, matching the web
24
- // input's `aria-label`.
1
+ // `<DateTimePicker>` (native). Binds ./datetimepicker-view.js to React Native
2
+ // with the operating system's own picker as its surface, via
3
+ // `@react-native-community/datetimepicker`. That library is imperative on
4
+ // Android — rendering it opens the dialog — so it mounts only while open.
25
5
 
26
- import React, { useMemo, useState } from "react";
6
+ import React from "react";
27
7
  // eslint-disable-next-line no-restricted-syntax
28
8
  import RNDateTimePicker from "@react-native-community/datetimepicker";
29
- import { Pressable, Text, StyleSheet } from "react-native";
30
- import {
31
- parseToDate,
32
- formatToIso,
33
- formatDisplayLabel,
34
- } from "./datetimepicker-format.js";
35
- import { useHostTheme } from "./hooks.js";
36
- import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
37
-
38
- const styles = StyleSheet.create({
39
- trigger: {
40
- minHeight: 44,
41
- paddingVertical: 8,
42
- paddingHorizontal: 12,
43
- borderWidth: 1,
44
- borderRadius: 6,
45
- justifyContent: "center",
46
- },
47
- triggerDisabled: { opacity: 0.5 },
48
- label: { fontSize: 16 },
49
- placeholder: { fontSize: 16, opacity: 0.5 },
50
- });
51
-
52
- export function DateTimePicker({
53
- value,
54
- onChange,
55
- mode,
56
- minimumDate,
57
- maximumDate,
58
- disabled,
59
- accessibilityLabel,
60
- }) {
61
- const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
62
- const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
63
- const [open, setOpen] = useState(false);
64
- const dateValue = useMemo(() => parseToDate(value), [value]);
65
- const min = useMemo(
66
- () => (minimumDate ? parseToDate(minimumDate) : undefined),
67
- [minimumDate],
68
- );
69
- const max = useMemo(
70
- () => (maximumDate ? parseToDate(maximumDate) : undefined),
71
- [maximumDate],
72
- );
73
-
74
- const isEmpty = value == null || value === "";
75
-
76
- const handleChange = (event, picked) => {
77
- // Android fires "dismissed" when the user cancels; close without emitting.
78
- setOpen(false);
79
- if (event?.type === "dismissed") return;
80
- if (typeof onChange !== "function") return;
81
- if (!(picked instanceof Date)) return;
82
- const iso = formatToIso(picked, effectiveMode);
83
- if (iso != null) onChange(iso);
84
- };
9
+ import { View, Text, Pressable, Platform } from "react-native";
10
+ import { makeDateTimePicker } from "./datetimepicker-view.js";
11
+ import { nowParts, partsToDate, valueToParts } from "./datetimepicker-format.js";
12
+ import { clampToBounds } from "./datetimepicker-calendar.js";
13
+
14
+ function boundDate(parts, mode) {
15
+ if (!parts || mode === "time") return undefined;
16
+ return partsToDate(parts);
17
+ }
85
18
 
86
- const trigger = React.createElement(
87
- Pressable,
88
- {
89
- onPress: () => { if (!disabled) setOpen(true); },
90
- disabled: !!disabled,
91
- accessibilityRole: "button",
92
- accessibilityLabel,
93
- accessibilityState: { disabled: !!disabled },
94
- // Themed border + text so the trigger is legible on any surface (sc-3370).
95
- style: [
96
- styles.trigger,
97
- { borderColor: theme.colors.border },
98
- disabled && styles.triggerDisabled,
99
- ],
19
+ function OsPicker({ open, onClose, mode, parts, bounds, onPick }) {
20
+ if (!open) return null;
21
+ const start = parts ? { ...nowParts(), ...dropEmpty(parts) } : clampToBounds(nowParts(), bounds);
22
+ return React.createElement(RNDateTimePicker, {
23
+ value: partsToDate(start),
24
+ // iOS honours "datetime"; Android shows its date dialog for it.
25
+ mode,
26
+ minimumDate: boundDate(bounds.min, mode),
27
+ maximumDate: boundDate(bounds.max, mode),
28
+ onChange: (event, picked) => {
29
+ onClose();
30
+ // Android reports a cancelled dialog as "dismissed".
31
+ if (event && event.type === "dismissed") return;
32
+ if (!(picked instanceof Date)) return;
33
+ // Android's time dialog cannot be bounded, so a pick is clamped here.
34
+ onPick(clampToBounds(valueToParts(picked.toISOString(), "datetime"), bounds));
100
35
  },
101
- React.createElement(
102
- Text,
103
- {
104
- style: [
105
- isEmpty ? styles.placeholder : styles.label,
106
- { color: theme.colors.onSurface },
107
- ],
108
- },
109
- formatDisplayLabel(value, effectiveMode),
110
- ),
111
- );
112
-
113
- if (!open) return trigger;
36
+ });
37
+ }
114
38
 
115
- // The RN library's `mode` is "date" / "time"; iOS also honours "datetime",
116
- // Android falls back to date-only for it (a pre-existing limit, unchanged
117
- // here). The web build uses datetimepicker.js, so this file is never web.
118
- return React.createElement(
119
- React.Fragment,
120
- null,
121
- trigger,
122
- React.createElement(RNDateTimePicker, {
123
- value: dateValue,
124
- mode: effectiveMode,
125
- minimumDate: min,
126
- maximumDate: max,
127
- disabled: !!disabled,
128
- onChange: handleChange,
129
- }),
130
- );
39
+ function dropEmpty(parts) {
40
+ return Object.fromEntries(Object.entries(parts).filter(([, v]) => v != null));
131
41
  }
42
+
43
+ export const DateTimePicker = makeDateTimePicker({ View, Text, Pressable, Platform, Surface: OsPicker });
package/dist/hooks.js CHANGED
@@ -238,6 +238,14 @@ export function useHostNavigation() {
238
238
  return ctx ? ctx.navigation : undefined;
239
239
  }
240
240
 
241
+ // @internal — the app's language, read WITHOUT asserting a widget context so
242
+ // `<DateTimePicker>` can name months in it wherever it renders.
243
+ export function useHostLocale() {
244
+ const ctx = useContext(HostWidgetContext);
245
+ const locale = ctx && ctx.i18n ? ctx.i18n.locale : undefined;
246
+ return typeof locale === "string" && locale ? locale : undefined;
247
+ }
248
+
241
249
  // A host translation, or null on a miss: both hosts answer a miss with the
242
250
  // key itself or a `{{t:…}}` placeholder.
243
251
  function hostTranslation(hostT, key) {
package/dist/host.d.ts CHANGED
@@ -578,6 +578,12 @@ export function resolveLoginLogoUrl(
578
578
  appLogoUrl: string | null | undefined,
579
579
  ): string | null;
580
580
 
581
+ /** sc-11754 — whether the sign-in card shows its heading and subheading. */
582
+ export function resolveLoginTextVisibility(login: unknown): {
583
+ heading: boolean;
584
+ subheading: boolean;
585
+ };
586
+
581
587
  /** sc-7800 — true when a stored value is a `theme:<path>` colour binding. */
582
588
  export function isThemeTokenRef(value: unknown): boolean;
583
589
 
@@ -626,10 +632,12 @@ export function isoToDateTimeLocalInput(value: unknown): string;
626
632
  export function dateTimeLocalInputToIso(raw: unknown): string;
627
633
 
628
634
  /**
629
- * sc-8448 — the `<DateTimePicker>` trigger label: for `"datetime"` the stored
630
- * instant as `YYYY-MM-DD HH:mm` local time, falling back to the raw string.
635
+ * The `<DateTimePicker>` field's label: the value in `locale` (default: the
636
+ * runtime's), a `"datetime"` instant on the viewer's clock (sc-8448, sc-11777).
637
+ * Empty gives the mode's placeholder; an unreadable value is returned as is.
631
638
  */
632
639
  export function formatDateTimeLabel(
633
640
  value: unknown,
634
641
  mode: "date" | "time" | "datetime",
642
+ locale?: string,
635
643
  ): string;
package/dist/host.js CHANGED
@@ -145,6 +145,7 @@ export {
145
145
  normaliseThemeGradient,
146
146
  normaliseLoginDesign,
147
147
  resolveLoginLogoUrl,
148
+ resolveLoginTextVisibility,
148
149
  } from "./theme-background.js";
149
150
 
150
151
  // sc-8448: one DATETIME local-clock conversion for the picker and the Studio.
@@ -153,9 +153,27 @@ function resolveLoginLogoUrl(login, appLogoUrl) {
153
153
  return own || normaliseImageUrl(appLogoUrl);
154
154
  }
155
155
 
156
+ /**
157
+ * sc-11754 — which of the card's two text lines the author left showing.
158
+ * Only a literal `true` hides, so every app saved before this keeps both.
159
+ * Not in `normaliseLoginDesign`: hidden text is not a backdrop, so it must
160
+ * not lift the card or drop the shell background.
161
+ *
162
+ * @param {object|null|undefined} login - `themeConfig.login`.
163
+ * @returns {{ heading: boolean, subheading: boolean }}
164
+ */
165
+ function resolveLoginTextVisibility(login) {
166
+ const own = login && typeof login === "object" ? login : {};
167
+ return {
168
+ heading: own.hideHeading !== true,
169
+ subheading: own.hideSubheading !== true,
170
+ };
171
+ }
172
+
156
173
  module.exports = {
157
174
  LOGIN_TEXT_MAX_LENGTH,
158
175
  normaliseThemeGradient,
159
176
  normaliseLoginDesign,
160
177
  resolveLoginLogoUrl,
178
+ resolveLoginTextVisibility,
161
179
  };
@@ -144,3 +144,20 @@ export function resolveLoginLogoUrl(login, appLogoUrl) {
144
144
  : null;
145
145
  return own || normaliseImageUrl(appLogoUrl);
146
146
  }
147
+
148
+ /**
149
+ * sc-11754 — which of the card's two text lines the author left showing.
150
+ * Only a literal `true` hides, so every app saved before this keeps both.
151
+ * Not in `normaliseLoginDesign`: hidden text is not a backdrop, so it must
152
+ * not lift the card or drop the shell background.
153
+ *
154
+ * @param {object|null|undefined} login - `themeConfig.login`.
155
+ * @returns {{ heading: boolean, subheading: boolean }}
156
+ */
157
+ export function resolveLoginTextVisibility(login) {
158
+ const own = login && typeof login === "object" ? login : {};
159
+ return {
160
+ heading: own.hideHeading !== true,
161
+ subheading: own.hideSubheading !== true,
162
+ };
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.234.0",
3
+ "version": "0.236.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",