@colixsystems/widget-sdk 0.62.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
53
53
 
54
54
  ## Status
55
55
 
56
- `v0.62.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
56
+ `v0.64.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
57
+
58
+ ### What's new in 0.64.0
59
+
60
+ **`<DateTimePicker>` themes itself — legible on dark surfaces (sc-3370).** The primitive now derives its colours from the workspace theme on BOTH hosts instead of hardcoding them: the text uses `colors.onSurface`, the border uses `colors.border`, and on web the input's `color-scheme` follows the theme so the browser's built-in date UI (the `yyyy-mm-dd` edit segments and the calendar icon) stays legible on a dark surface. Previously the web input used `color: inherit` with no `color-scheme` and the native trigger set no text colour, so on a dark-themed app the field rendered dark-on-dark and unreadable — with no prop an author could set to fix it. **No prop changed**: the seven-prop contract (`value, onChange, mode, minimumDate, maximumDate, disabled, accessibilityLabel`) is unchanged, and you do NOT style the field yourself — never reach for raw CSS or `document`, which the widget linter rejects. `CONTRACT.version` → `1.43.0`. Behavioural fix, additive.
61
+
62
+ ### What's new in 0.63.0
63
+
64
+ **New optional manifest field `rendersOwnChrome` (sc-3331).** A boolean (default `false`) that declares whether your widget renders its OWN section header — a heading (and optional subtitle), plus any primary action for its section — making it a self-contained section. Set it `true` when your widget draws its own title (from a `title`/`subtitle` prop with a real default, so the author can still retitle it in the Properties Panel), and the AppStudio app-builder will place the widget as the WHOLE section: it will NOT add a standalone heading or a duplicate action button above it, so the section is never double-titled. Leave it `false`/omitted for a content-only widget whose heading the page supplies. `CONTRACT.version` → `1.42.0`. Additive — existing manifests omit it and read `false`, so no widget needs changing.
57
65
 
58
66
  ### What's new in 0.62.0
59
67
 
package/dist/contract.cjs CHANGED
@@ -823,7 +823,7 @@ const PRIMITIVES = [
823
823
  {
824
824
  name: "DateTimePicker",
825
825
  description:
826
- 'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native `<input type="date|time|datetime-local">` directly (react-native-web has no mapping for the RN datetimepicker library); native uses @react-native-community/datetimepicker.',
826
+ 'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native `<input type="date|time|datetime-local">` directly (react-native-web has no mapping for the RN datetimepicker library); native uses @react-native-community/datetimepicker. The field themes itself from the workspace theme — text uses `colors.onSurface`, the border `colors.border`, and on web `color-scheme` follows the theme so the browser\'s built-in date UI stays legible on a dark surface; never style it with raw CSS or `document`.',
827
827
  rnComponent: "@react-native-community/datetimepicker",
828
828
  docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
829
829
  },
@@ -971,6 +971,13 @@ const MANIFEST_SCHEMA = {
971
971
  "never auto-applies style — the widget owns placement.",
972
972
  default: {},
973
973
  },
974
+ rendersOwnChrome: {
975
+ type: "boolean",
976
+ required: false,
977
+ description:
978
+ "Optional (default false). TRUE when the widget renders its OWN section header — a heading (and optional subtitle), plus any primary action for its section — making it a self-contained section. The AppStudio app-builder reads this when it places an already-installed widget: it lays the widget out as the WHOLE section and does NOT add a standalone heading or a duplicate action button above it, so the section is never double-titled. Set it to match what the component actually renders; leave it false (or omit) for a content-only widget whose heading the page supplies.",
979
+ default: false,
980
+ },
974
981
  events: {
975
982
  type: "object[]",
976
983
  required: true,
@@ -1908,7 +1915,18 @@ const CONTRACT = deepFreeze({
1908
1915
  // no in-code `|| "fallback"`. Backed by a host-only subpath export
1909
1916
  // (`@colixsystems/widget-sdk/host` -> resolveProps); the author-facing entry
1910
1917
  // is unchanged. No existing behaviour changes — minor bump.
1911
- version: "1.41.0",
1918
+ // 1.42.0 (sc-3331) — new OPTIONAL manifest field `rendersOwnChrome` (boolean,
1919
+ // default false): declares that the widget renders its own section header,
1920
+ // so the app-builder places it as a whole section without stacking a
1921
+ // heading above it. Additive — existing manifests omit it and read false.
1922
+ // 1.43.0 (sc-3370) — the `<DateTimePicker>` primitive now themes itself from
1923
+ // the workspace theme on both hosts: text uses `colors.onSurface`, the
1924
+ // border `colors.border`, and the web input's `color-scheme` follows the
1925
+ // theme so the browser's built-in date UI stays legible on a dark surface.
1926
+ // Previously it hardcoded `color: inherit` / an unset scheme (web) and an
1927
+ // uncoloured trigger (native), rendering dark-on-dark. No prop changed —
1928
+ // behavioural fix, additive.
1929
+ version: "1.43.0",
1912
1930
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1913
1931
  hooks: HOOKS,
1914
1932
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -823,7 +823,7 @@ const PRIMITIVES = [
823
823
  {
824
824
  name: "DateTimePicker",
825
825
  description:
826
- 'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native `<input type="date|time|datetime-local">` directly (react-native-web has no mapping for the RN datetimepicker library); native uses @react-native-community/datetimepicker.',
826
+ 'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native `<input type="date|time|datetime-local">` directly (react-native-web has no mapping for the RN datetimepicker library); native uses @react-native-community/datetimepicker. The field themes itself from the workspace theme — text uses `colors.onSurface`, the border `colors.border`, and on web `color-scheme` follows the theme so the browser\'s built-in date UI stays legible on a dark surface; never style it with raw CSS or `document`.',
827
827
  rnComponent: "@react-native-community/datetimepicker",
828
828
  docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
829
829
  },
@@ -971,6 +971,13 @@ const MANIFEST_SCHEMA = {
971
971
  "never auto-applies style — the widget owns placement.",
972
972
  default: {},
973
973
  },
974
+ rendersOwnChrome: {
975
+ type: "boolean",
976
+ required: false,
977
+ description:
978
+ "Optional (default false). TRUE when the widget renders its OWN section header — a heading (and optional subtitle), plus any primary action for its section — making it a self-contained section. The AppStudio app-builder reads this when it places an already-installed widget: it lays the widget out as the WHOLE section and does NOT add a standalone heading or a duplicate action button above it, so the section is never double-titled. Set it to match what the component actually renders; leave it false (or omit) for a content-only widget whose heading the page supplies.",
979
+ default: false,
980
+ },
974
981
  events: {
975
982
  type: "object[]",
976
983
  required: true,
@@ -1908,7 +1915,18 @@ const CONTRACT = deepFreeze({
1908
1915
  // no in-code `|| "fallback"`. Backed by a host-only subpath export
1909
1916
  // (`@colixsystems/widget-sdk/host` -> resolveProps); the author-facing entry
1910
1917
  // is unchanged. No existing behaviour changes — minor bump.
1911
- version: "1.41.0",
1918
+ // 1.42.0 (sc-3331) — new OPTIONAL manifest field `rendersOwnChrome` (boolean,
1919
+ // default false): declares that the widget renders its own section header,
1920
+ // so the app-builder places it as a whole section without stacking a
1921
+ // heading above it. Additive — existing manifests omit it and read false.
1922
+ // 1.43.0 (sc-3370) — the `<DateTimePicker>` primitive now themes itself from
1923
+ // the workspace theme on both hosts: text uses `colors.onSurface`, the
1924
+ // border `colors.border`, and the web input's `color-scheme` follows the
1925
+ // theme so the browser's built-in date UI stays legible on a dark surface.
1926
+ // Previously it hardcoded `color: inherit` / an unset scheme (web) and an
1927
+ // uncoloured trigger (native), rendering dark-on-dark. No prop changed —
1928
+ // behavioural fix, additive.
1929
+ version: "1.43.0",
1912
1930
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1913
1931
  hooks: HOOKS,
1914
1932
  primitives: PRIMITIVES,
@@ -1,9 +1,9 @@
1
- // REQ-WSDK-PLATFORM §6 — pure date/ISO helpers for the native `<DateTimePicker>`.
1
+ // REQ-WSDK-PLATFORM §6 — pure, RN-free helpers for the `<DateTimePicker>`.
2
2
  //
3
- // Split out of datetimepicker.native.js so the formatting contract can be
3
+ // Split out of the primitive so the formatting + theming contract can be
4
4
  // unit-tested without importing react-native / the RN datetimepicker library
5
5
  // (neither is installed in this package's node tree). No React, no RN here —
6
- // just Date math and string shaping.
6
+ // just Date math, string shaping, and a colour-luminance test.
7
7
 
8
8
  export function parseToDate(value) {
9
9
  if (value == null || value === "") return new Date();
@@ -51,3 +51,22 @@ export function formatDisplayLabel(value, mode) {
51
51
  if (Number.isNaN(d.getTime())) return String(value);
52
52
  return `${formatToIso(d, "date")} ${formatToIso(d, "time")}`;
53
53
  }
54
+
55
+ // Perceived-luminance test for a hex colour (`#rgb` or `#rrggbb`). The web
56
+ // picker uses it to choose a matching `color-scheme` so the browser's built-in
57
+ // date UI (edit segments + calendar icon) stays legible on a dark themed
58
+ // surface. Anything unparseable is treated as light so a light theme is never
59
+ // darkened.
60
+ export function isDarkColor(hex) {
61
+ if (typeof hex !== "string") return false;
62
+ let h = hex.trim().replace(/^#/, "");
63
+ if (h.length === 3) {
64
+ h = h.split("").map((c) => c + c).join("");
65
+ }
66
+ if (h.length !== 6 || /[^0-9a-fA-F]/.test(h)) return false;
67
+ const r = parseInt(h.slice(0, 2), 16);
68
+ const g = parseInt(h.slice(2, 4), 16);
69
+ const b = parseInt(h.slice(4, 6), 16);
70
+ const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
71
+ return luminance < 0.5;
72
+ }
@@ -25,6 +25,9 @@
25
25
  // readers can locate the input by its label.
26
26
 
27
27
  import React from "react";
28
+ import { useHostTheme } from "./hooks.js";
29
+ import { isDarkColor } from "./datetimepicker-format.js";
30
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
28
31
 
29
32
  const MODE_TO_INPUT_TYPE = {
30
33
  date: "date",
@@ -80,6 +83,7 @@ export function DateTimePicker({
80
83
  disabled,
81
84
  accessibilityLabel,
82
85
  }) {
86
+ const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
83
87
  const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
84
88
  const inputType = MODE_TO_INPUT_TYPE[effectiveMode];
85
89
  const inputValue = _isoToInputValue(value, effectiveMode);
@@ -128,7 +132,13 @@ export function DateTimePicker({
128
132
  // Inline styles match the SDK's other web-only primitives — the host's
129
133
  // form widgets wrap this in their own labelled field, so the input just
130
134
  // needs to look like a normal text input. `cursor: pointer` signals the
131
- // whole field is the click affordance (sc-1878).
135
+ // whole field is the click affordance (sc-1878). Colours come from the
136
+ // theme so the field is legible on any surface (sc-3370); `colorScheme`
137
+ // steers the browser's built-in date UI (edit segments + calendar icon).
138
+ // The scheme keys off the theme's TEXT colour (onSurface, which the host
139
+ // already picked to contrast the surface) rather than re-deriving dark-vs-
140
+ // light from the surface with a second luminance formula that could disagree
141
+ // with the host: light text ⇒ dark surface ⇒ dark scheme.
132
142
  const style = {
133
143
  boxSizing: "border-box",
134
144
  width: "100%",
@@ -136,11 +146,12 @@ export function DateTimePicker({
136
146
  padding: "8px 12px",
137
147
  fontSize: 16,
138
148
  fontFamily: "inherit",
139
- color: "inherit",
149
+ color: theme.colors.onSurface,
140
150
  backgroundColor: "transparent",
151
+ colorScheme: isDarkColor(theme.colors.onSurface) ? "light" : "dark",
141
152
  borderWidth: 1,
142
153
  borderStyle: "solid",
143
- borderColor: "rgba(0, 0, 0, 0.16)",
154
+ borderColor: theme.colors.border,
144
155
  borderRadius: 6,
145
156
  outline: "none",
146
157
  cursor: disabled ? "default" : "pointer",
@@ -32,6 +32,8 @@ import {
32
32
  formatToIso,
33
33
  formatDisplayLabel,
34
34
  } from "./datetimepicker-format.js";
35
+ import { useHostTheme } from "./hooks.js";
36
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
35
37
 
36
38
  const styles = StyleSheet.create({
37
39
  trigger: {
@@ -39,7 +41,6 @@ const styles = StyleSheet.create({
39
41
  paddingVertical: 8,
40
42
  paddingHorizontal: 12,
41
43
  borderWidth: 1,
42
- borderColor: "rgba(0, 0, 0, 0.16)",
43
44
  borderRadius: 6,
44
45
  justifyContent: "center",
45
46
  },
@@ -57,6 +58,7 @@ export function DateTimePicker({
57
58
  disabled,
58
59
  accessibilityLabel,
59
60
  }) {
61
+ const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
60
62
  const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
61
63
  const [open, setOpen] = useState(false);
62
64
  const dateValue = useMemo(() => parseToDate(value), [value]);
@@ -89,11 +91,21 @@ export function DateTimePicker({
89
91
  accessibilityRole: "button",
90
92
  accessibilityLabel,
91
93
  accessibilityState: { disabled: !!disabled },
92
- style: [styles.trigger, disabled && styles.triggerDisabled],
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
+ ],
93
100
  },
94
101
  React.createElement(
95
102
  Text,
96
- { style: isEmpty ? styles.placeholder : styles.label },
103
+ {
104
+ style: [
105
+ isEmpty ? styles.placeholder : styles.label,
106
+ { color: theme.colors.onSurface },
107
+ ],
108
+ },
97
109
  formatDisplayLabel(value, effectiveMode),
98
110
  ),
99
111
  );
package/dist/hooks.js CHANGED
@@ -91,6 +91,17 @@ export function useTheme() {
91
91
  return ctx.workspace.theme;
92
92
  }
93
93
 
94
+ // @internal — reads the host theme WITHOUT throwing when no provider is
95
+ // mounted (returns undefined instead), unlike useTheme() which asserts a
96
+ // widget context. Low-level primitives (DateTimePicker) read it so they stay
97
+ // renderable anywhere, applying their own default tokens on the undefined
98
+ // case. Not part of the public widget hook surface — not re-exported from
99
+ // index.js.
100
+ export function useHostTheme() {
101
+ const ctx = useContext(HostWidgetContext);
102
+ return ctx && ctx.workspace ? ctx.workspace.theme : undefined;
103
+ }
104
+
94
105
  /**
95
106
  * REQ-THEME-13 — returns the author-set per-widget style values: the object the
96
107
  * host delivers under `props.style`, keyed by the style-field names the widget
@@ -2823,16 +2834,37 @@ function toPaymentError(err) {
2823
2834
  * anything yourself (no `Linking.openURL`, no `checkout_url` to
2824
2835
  * handle). The mock provider auto-confirms (`status: "PAID"`, no
2825
2836
  * redirect). Rejects with a `PaymentError`.
2826
- * getPayment(paymentId) → Promise<payment> — poll the terminal status.
2827
- *
2828
- * Because the web redirect reloads the app on return, confirm completion
2829
- * from server-authoritative state (the Mollie webhook flips the charge /
2830
- * your datastore record) re-read your bound record on mount, or poll
2831
- * `getPayment(id)` after the mock/native flow resolves. Requires the
2832
- * `payments.charge:appUser` scope in the manifest's `requestedScopes`. The
2833
- * charge settles to the workspace owner; the app user confirms the amount
2834
- * in hosted Checkout. No card data touches the widget — never collect card
2835
- * fields yourself.
2837
+ * getPayment(paymentId) → Promise<payment> — read the current status
2838
+ * (`PENDING` | `PAID` | `FAILED`), scoped server-side to the caller.
2839
+ *
2840
+ * THE PAYER IS ALWAYS A SIGNED-IN APP USER charging requires it, and a paid
2841
+ * widget lives on a page behind login. So read the buyer's identity from
2842
+ * `useUser()` (`{ id, email, displayName }`) and connect the record you write
2843
+ * to `user.id`. Do NOT re-collect the name / email `useUser()` already gives
2844
+ * you as required inputs; pre-filling an editable field from it, or asking for
2845
+ * genuinely new details (phone, address, notes), is fine.
2846
+ *
2847
+ * CONFIRM FROM SERVER STATE, NOT MEMORY — and handle success, failure, AND
2848
+ * cancel. On web `requestPayment` triggers a full-page redirect, so the app
2849
+ * reloads on return and any in-memory "done" flag is gone. One pattern works on
2850
+ * both platforms:
2851
+ * 1. On mount, read `useRouteParams()`. After the web redirect it carries
2852
+ * `payment` (`"success"` | `"cancel"`) and, on success, `payment_id`.
2853
+ * `"cancel"` → the user backed out: show a "payment cancelled" state, do
2854
+ * NOT treat it as paid. `"success"` + `payment_id` → poll `getPayment(id)`
2855
+ * until it leaves `PENDING`, then branch: `PAID` → confirm / write the
2856
+ * record; `FAILED` → show a failure state.
2857
+ * 2. Also poll the `id` that `requestPayment` RESOLVES with — the mock
2858
+ * provider and the native in-app-browser flow return here without a reload,
2859
+ * so route params are absent and this branch reconciles instead.
2860
+ * Poll a bounded number of times (e.g. every ~1.5s for up to ~30s) because the
2861
+ * Mollie webhook may land a moment after the redirect; if it is still `PENDING`
2862
+ * when you give up, show a "still processing" state rather than "paid".
2863
+ *
2864
+ * Requires the `payments.charge:appUser` scope in the manifest's
2865
+ * `requestedScopes`. The charge settles to the workspace owner; the app user
2866
+ * confirms the amount in hosted Checkout. No card data touches the widget —
2867
+ * never collect card fields yourself.
2836
2868
  */
2837
2869
  export function usePayments() {
2838
2870
  const ctx = useWidgetContextOrThrow("usePayments");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.62.0",
3
+ "version": "0.64.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",