@colixsystems/widget-sdk 0.13.0 → 0.15.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/dist/hooks.js CHANGED
@@ -61,7 +61,9 @@ export function WidgetContextProvider({ value, children }) {
61
61
  return React.createElement(HostWidgetContext.Provider, { value }, children);
62
62
  }
63
63
 
64
- function useWidgetContextOrThrow(hookName) {
64
+ // Exported for ./toast.js + future hook modules outside hooks.js. Internal
65
+ // utility — widget code should not import this directly.
66
+ export function useWidgetContextOrThrow(hookName) {
65
67
  const ctx = useContext(HostWidgetContext);
66
68
  if (ctx == null) {
67
69
  throw new Error(
@@ -687,6 +689,236 @@ export function usePayments() {
687
689
  * the key is missing. The host's i18n.t may or may not honour the two-arg
688
690
  * form; we degrade gracefully either way.
689
691
  */
692
+ /**
693
+ * REQ-USERMGMT / REQ-ACL-SYS M3 — structured error thrown by `useUsers` /
694
+ * `useGroups` callbacks. Carries a stable `code` so widgets can branch
695
+ * without parsing message strings.
696
+ *
697
+ * `code` is one of:
698
+ * - "FORBIDDEN" — 403 from the host (capability or scope missing).
699
+ * - "VALIDATION" — 400 / 422 (bad email, duplicate, etc.).
700
+ * - "NOT_FOUND" — 404 (group / user does not exist or cross-tenant).
701
+ * - "INVITE_ONLY" — 409 from accept-invite-style flows where the tenant
702
+ * is invite-only and the email is not on the list.
703
+ * - "INTERNAL" — anything else (network, 5xx).
704
+ */
705
+ export class DirectoryError extends Error {
706
+ constructor(code, message, opts) {
707
+ super(message);
708
+ this.name = "DirectoryError";
709
+ this.code = code;
710
+ if (opts && opts.cause) this.cause = opts.cause;
711
+ }
712
+ }
713
+
714
+ function toDirectoryError(err) {
715
+ if (err instanceof DirectoryError) return err;
716
+ const status =
717
+ err && err.response && typeof err.response.status === "number"
718
+ ? err.response.status
719
+ : null;
720
+ const bodyCode =
721
+ err && err.response && err.response.data && err.response.data.code;
722
+ const bodyMessage =
723
+ err && err.response && err.response.data && err.response.data.error;
724
+ let code = "INTERNAL";
725
+ if (bodyCode === "INVITE_ONLY") code = "INVITE_ONLY";
726
+ else if (status === 403) code = "FORBIDDEN";
727
+ else if (status === 404) code = "NOT_FOUND";
728
+ else if (status === 400 || status === 422) code = "VALIDATION";
729
+ else if (status === 409) {
730
+ // 409 is invite-only on the invite endpoints; treat the rest as
731
+ // validation conflicts (duplicate email, etc.).
732
+ code = bodyCode === "INVITE_ONLY" ? "INVITE_ONLY" : "VALIDATION";
733
+ }
734
+ const message =
735
+ (typeof bodyMessage === "string" && bodyMessage) ||
736
+ (err && typeof err.message === "string"
737
+ ? err.message
738
+ : "Directory call failed");
739
+ return new DirectoryError(code, message, { cause: err });
740
+ }
741
+
742
+ /**
743
+ * REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration hook.
744
+ *
745
+ * Returns `{ users, loading, error, refetch, invite, deactivate,
746
+ * reactivate, remove }`. The list refetches whenever
747
+ * `JSON.stringify(query)` changes; the imperative methods reject with a
748
+ * `DirectoryError`. Reads require the `users.read:*` scope; mutations
749
+ * additionally require `users.write:*`. The host's signed
750
+ * `X-Widget-Scopes` header + a tenant-scoped SystemAcl `users.read` /
751
+ * `users.write` capability grant gate the underlying endpoint
752
+ * (REQ-ACL-SYS M3 §4.3) — a widget that declares the scopes but whose
753
+ * caller does not hold the grant gets a `FORBIDDEN`.
754
+ */
755
+ export function useUsers(query) {
756
+ const ctx = useWidgetContextOrThrow("useUsers");
757
+ if (!ctx.users || typeof ctx.users.listUsers !== "function") {
758
+ throw new Error("useUsers: host did not inject a users client");
759
+ }
760
+ const [users, setUsers] = useState([]);
761
+ const [loading, setLoading] = useState(true);
762
+ const [error, setError] = useState(null);
763
+
764
+ const queryRef = useRef(query);
765
+ const usersRef = useRef(ctx.users);
766
+ queryRef.current = query;
767
+ usersRef.current = ctx.users;
768
+
769
+ const runRef = useRef(0);
770
+
771
+ const doFetch = useCallback(async () => {
772
+ const myRun = ++runRef.current;
773
+ setLoading(true);
774
+ setError(null);
775
+ try {
776
+ const rows = await usersRef.current.listUsers(queryRef.current);
777
+ if (runRef.current !== myRun) return;
778
+ setUsers(Array.isArray(rows) ? rows : []);
779
+ setLoading(false);
780
+ } catch (err) {
781
+ if (runRef.current !== myRun) return;
782
+ setError(toDirectoryError(err));
783
+ setLoading(false);
784
+ }
785
+ }, []);
786
+
787
+ const queryKey = (() => {
788
+ try {
789
+ return JSON.stringify(query);
790
+ } catch (_e) {
791
+ return null;
792
+ }
793
+ })();
794
+ useEffect(() => {
795
+ doFetch();
796
+ // eslint-disable-next-line react-hooks/exhaustive-deps
797
+ }, [queryKey]);
798
+
799
+ const refetch = useCallback(async () => {
800
+ await doFetch();
801
+ }, [doFetch]);
802
+
803
+ const invite = useCallback(async (args) => {
804
+ try {
805
+ return await usersRef.current.invite(args);
806
+ } catch (err) {
807
+ throw toDirectoryError(err);
808
+ }
809
+ }, []);
810
+ const deactivate = useCallback(async (userId) => {
811
+ try {
812
+ return await usersRef.current.deactivate(userId);
813
+ } catch (err) {
814
+ throw toDirectoryError(err);
815
+ }
816
+ }, []);
817
+ const reactivate = useCallback(async (userId) => {
818
+ try {
819
+ return await usersRef.current.reactivate(userId);
820
+ } catch (err) {
821
+ throw toDirectoryError(err);
822
+ }
823
+ }, []);
824
+ const remove = useCallback(async (userId) => {
825
+ try {
826
+ return await usersRef.current.remove(userId);
827
+ } catch (err) {
828
+ throw toDirectoryError(err);
829
+ }
830
+ }, []);
831
+
832
+ return { users, loading, error, refetch, invite, deactivate, reactivate, remove };
833
+ }
834
+
835
+ /**
836
+ * REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration hook.
837
+ *
838
+ * Returns `{ groups, loading, error, refetch, create, remove, addMember,
839
+ * removeMember }`. Reads require `groups.read:*`; mutations require
840
+ * `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as `useUsers`.
841
+ */
842
+ export function useGroups(query) {
843
+ const ctx = useWidgetContextOrThrow("useGroups");
844
+ if (!ctx.groups || typeof ctx.groups.listGroups !== "function") {
845
+ throw new Error("useGroups: host did not inject a groups client");
846
+ }
847
+ const [groups, setGroups] = useState([]);
848
+ const [loading, setLoading] = useState(true);
849
+ const [error, setError] = useState(null);
850
+
851
+ const queryRef = useRef(query);
852
+ const groupsRef = useRef(ctx.groups);
853
+ queryRef.current = query;
854
+ groupsRef.current = ctx.groups;
855
+
856
+ const runRef = useRef(0);
857
+
858
+ const doFetch = useCallback(async () => {
859
+ const myRun = ++runRef.current;
860
+ setLoading(true);
861
+ setError(null);
862
+ try {
863
+ const rows = await groupsRef.current.listGroups(queryRef.current);
864
+ if (runRef.current !== myRun) return;
865
+ setGroups(Array.isArray(rows) ? rows : []);
866
+ setLoading(false);
867
+ } catch (err) {
868
+ if (runRef.current !== myRun) return;
869
+ setError(toDirectoryError(err));
870
+ setLoading(false);
871
+ }
872
+ }, []);
873
+
874
+ const queryKey = (() => {
875
+ try {
876
+ return JSON.stringify(query);
877
+ } catch (_e) {
878
+ return null;
879
+ }
880
+ })();
881
+ useEffect(() => {
882
+ doFetch();
883
+ // eslint-disable-next-line react-hooks/exhaustive-deps
884
+ }, [queryKey]);
885
+
886
+ const refetch = useCallback(async () => {
887
+ await doFetch();
888
+ }, [doFetch]);
889
+
890
+ const create = useCallback(async (args) => {
891
+ try {
892
+ return await groupsRef.current.create(args);
893
+ } catch (err) {
894
+ throw toDirectoryError(err);
895
+ }
896
+ }, []);
897
+ const remove = useCallback(async (groupId) => {
898
+ try {
899
+ return await groupsRef.current.remove(groupId);
900
+ } catch (err) {
901
+ throw toDirectoryError(err);
902
+ }
903
+ }, []);
904
+ const addMember = useCallback(async (groupId, userId) => {
905
+ try {
906
+ return await groupsRef.current.addMember(groupId, userId);
907
+ } catch (err) {
908
+ throw toDirectoryError(err);
909
+ }
910
+ }, []);
911
+ const removeMember = useCallback(async (groupId, userId) => {
912
+ try {
913
+ return await groupsRef.current.removeMember(groupId, userId);
914
+ } catch (err) {
915
+ throw toDirectoryError(err);
916
+ }
917
+ }, []);
918
+
919
+ return { groups, loading, error, refetch, create, remove, addMember, removeMember };
920
+ }
921
+
690
922
  export function useI18n() {
691
923
  const ctx = useWidgetContextOrThrow("useI18n");
692
924
  const i18n = ctx.i18n || {};
package/dist/icon.js ADDED
@@ -0,0 +1,29 @@
1
+ // REQ-WSDK-PLATFORM §6 — `<Icon>` SDK primitive.
2
+ //
3
+ // Wraps lucide-react-native's name-keyed component map behind a small
4
+ // stable API: `<Icon name="check" size={16} color="..." />`. Same source
5
+ // runs on both platforms — lucide-react-native ships a working web build
6
+ // (via the host's Vite shim) and a native build (Metro picks it
7
+ // straight). Unknown names fall back to the `Square` glyph so the
8
+ // canvas always shows something visible.
9
+ //
10
+ // The built-in `Icon` widget at frontend/src/components/widgets/Icon/
11
+ // already implements this exact pattern. Marketplace widgets reach for
12
+ // the SDK primitive instead of importing lucide-react-native directly,
13
+ // keeping the import-surface of a typical widget small (and avoiding the
14
+ // `import * as LucideIcons` pattern which trips eslint's
15
+ // no-restricted-syntax in some configs).
16
+
17
+ import React from "react";
18
+ import * as LucideIcons from "lucide-react-native";
19
+
20
+ export function Icon({ name, size, color }) {
21
+ const candidate =
22
+ typeof name === "string" && name.length > 0 ? LucideIcons[name] : null;
23
+ const Component = candidate || LucideIcons.Square;
24
+ const pixelSize = Number.isFinite(size) && size > 0 ? size : 24;
25
+ return React.createElement(Component, {
26
+ size: pixelSize,
27
+ color: color || "#0f172a",
28
+ });
29
+ }
package/dist/index.d.ts CHANGED
@@ -28,6 +28,9 @@ export type WidgetPropertyType =
28
28
  | "tableRef"
29
29
  | "columnRef"
30
30
  | "recordBinding"
31
+ // REQ-USERMGMT M4 / §4.8: Group picker that emits a bare
32
+ // AppUserGroup UUID into the page JSON.
33
+ | "groupRef"
31
34
  | "expression"
32
35
  | "eventBinding"
33
36
  | "object"
@@ -480,6 +483,109 @@ export class DatastoreError extends Error {
480
483
  );
481
484
  }
482
485
 
486
+ /**
487
+ * REQ-USERMGMT / REQ-ACL-SYS M3 — error class thrown by `useUsers` and
488
+ * `useGroups` callbacks. The `code` is a stable categorisation widgets
489
+ * can branch on.
490
+ */
491
+ export class DirectoryError extends Error {
492
+ code:
493
+ | "FORBIDDEN"
494
+ | "VALIDATION"
495
+ | "NOT_FOUND"
496
+ | "INVITE_ONLY"
497
+ | "INTERNAL";
498
+ constructor(
499
+ code: DirectoryError["code"],
500
+ message: string,
501
+ opts?: { cause?: unknown },
502
+ );
503
+ }
504
+
505
+ // --------------------------------------------------------------- useUsers
506
+ //
507
+ // REQ-USERMGMT / REQ-ACL-SYS M3 — AppUser administration hook.
508
+
509
+ export interface AppUserRow {
510
+ id: string;
511
+ name: string;
512
+ email?: string;
513
+ role: "USER" | "INTEGRATION";
514
+ isActive: boolean;
515
+ }
516
+
517
+ export interface UsersQuery {
518
+ q?: string;
519
+ role?: "USER" | "INTEGRATION" | "ALL";
520
+ isActive?: boolean;
521
+ limit?: number;
522
+ offset?: number;
523
+ }
524
+
525
+ export interface InviteArgs {
526
+ email: string;
527
+ name?: string;
528
+ groupIds?: string[];
529
+ }
530
+
531
+ export interface AppUserInviteRow {
532
+ id: string;
533
+ email: string;
534
+ status: string;
535
+ invitedAt?: string;
536
+ expiresAt?: string;
537
+ }
538
+
539
+ export interface UsersApi {
540
+ users: AppUserRow[];
541
+ loading: boolean;
542
+ error: DirectoryError | null;
543
+ refetch(): Promise<void>;
544
+ invite(args: InviteArgs): Promise<AppUserInviteRow>;
545
+ deactivate(userId: string): Promise<AppUserRow>;
546
+ reactivate(userId: string): Promise<AppUserRow>;
547
+ remove(userId: string): Promise<void>;
548
+ }
549
+
550
+ /**
551
+ * AppUser administration. Reads require the `users.read:*` scope in the
552
+ * manifest; mutations additionally require `users.write:*`. Widgets that
553
+ * declare the scopes but whose calling APP_USER lacks the corresponding
554
+ * SystemAcl `users.*` capability grant get a `FORBIDDEN` DirectoryError.
555
+ */
556
+ export function useUsers(query?: UsersQuery): UsersApi;
557
+
558
+ // --------------------------------------------------------------- useGroups
559
+
560
+ export interface AppUserGroupRow {
561
+ id: string;
562
+ name: string;
563
+ memberCount?: number;
564
+ }
565
+
566
+ export interface GroupsQuery {
567
+ q?: string;
568
+ limit?: number;
569
+ offset?: number;
570
+ }
571
+
572
+ export interface GroupsApi {
573
+ groups: AppUserGroupRow[];
574
+ loading: boolean;
575
+ error: DirectoryError | null;
576
+ refetch(): Promise<void>;
577
+ create(args: { name: string }): Promise<AppUserGroupRow>;
578
+ remove(groupId: string): Promise<void>;
579
+ addMember(groupId: string, userId: string): Promise<void>;
580
+ removeMember(groupId: string, userId: string): Promise<void>;
581
+ }
582
+
583
+ /**
584
+ * AppUserGroup administration. Reads require `groups.read:*`; mutations
585
+ * require `groups.write:*`. Same SystemAcl gating as `useUsers`.
586
+ */
587
+ export function useGroups(query?: GroupsQuery): GroupsApi;
588
+
483
589
  export function WidgetContextProvider(props: {
484
590
  value: WidgetContext;
485
591
  children?: ReactNode;
@@ -501,9 +607,29 @@ export const ActivityIndicator: any;
501
607
  export const Switch: any;
502
608
  export const StyleSheet: any;
503
609
 
610
+ /**
611
+ * REQ-WSDK-PLATFORM §6 — Lucide icon primitive. Unknown names fall back
612
+ * to the `Square` glyph so the canvas always shows something visible.
613
+ *
614
+ * @example
615
+ * import { Icon } from '@colixsystems/widget-sdk';
616
+ * <Icon name="check" size={16} color={theme.colors.primary} />
617
+ */
618
+ export const Icon: (props: {
619
+ name?: string;
620
+ size?: number;
621
+ color?: string;
622
+ }) => any;
623
+
504
624
  // Linter
505
625
  export interface LintFinding {
506
626
  rule: string;
627
+ /**
628
+ * REQ-WSDK-PLATFORM update: findings can now carry a `severity`. The
629
+ * lint's `ok` flag is true iff no `severity: "error"` finding exists.
630
+ * `severity: "warning"` findings surface to reviewers but do not block.
631
+ */
632
+ severity?: "error" | "warning";
507
633
  label: string;
508
634
  line: number;
509
635
  snippet: string;
package/dist/index.js CHANGED
@@ -9,11 +9,14 @@ export {
9
9
  WidgetContextProvider,
10
10
  DatastoreError,
11
11
  PaymentError,
12
+ DirectoryError,
12
13
  useDatastoreQuery,
13
14
  useDatastoreRecord,
14
15
  useFile,
15
16
  useDatastoreMutation,
16
17
  useDirectory,
18
+ useUsers,
19
+ useGroups,
17
20
  useWidgetEvent,
18
21
  usePayments,
19
22
  useTheme,
@@ -23,6 +26,11 @@ export {
23
26
  useChildRenderer,
24
27
  WidgetTree,
25
28
  } from "./hooks.js";
29
+ // REQ-WSDK-PLATFORM §6 — Tier A hooks. Each ships in a per-platform file
30
+ // (./clipboard.js / .native.js, ./toast.js / .native.js); index.js picks
31
+ // the web variant and index.native.js picks the native variant.
32
+ export { useClipboard, ClipboardError } from "./clipboard.js";
33
+ export { useToast } from "./toast.js";
26
34
  export {
27
35
  Text,
28
36
  View,
@@ -36,6 +44,8 @@ export {
36
44
  Switch,
37
45
  StyleSheet,
38
46
  Linking,
47
+ Icon,
48
+ DateTimePicker,
39
49
  } from "./primitives.js";
40
50
  export { lintSource, bannedIdentifiers } from "./linter.js";
41
51
  export { CONTRACT, isHookAllowed, requiredContextKeys } from "./contract.js";
@@ -9,11 +9,14 @@ export {
9
9
  WidgetContextProvider,
10
10
  DatastoreError,
11
11
  PaymentError,
12
+ DirectoryError,
12
13
  useDatastoreQuery,
13
14
  useDatastoreRecord,
14
15
  useFile,
15
16
  useDatastoreMutation,
16
17
  useDirectory,
18
+ useUsers,
19
+ useGroups,
17
20
  useWidgetEvent,
18
21
  usePayments,
19
22
  useTheme,
@@ -23,6 +26,9 @@ export {
23
26
  useChildRenderer,
24
27
  WidgetTree,
25
28
  } from "./hooks.js";
29
+ // REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
30
+ export { useClipboard, ClipboardError } from "./clipboard.native.js";
31
+ export { useToast } from "./toast.native.js";
26
32
  export {
27
33
  Text,
28
34
  View,
@@ -36,6 +42,8 @@ export {
36
42
  Switch,
37
43
  StyleSheet,
38
44
  Linking,
45
+ Icon,
46
+ DateTimePicker,
39
47
  } from "./primitives.native.js";
40
48
  export { lintSource, bannedIdentifiers } from "./linter.js";
41
49
  export { CONTRACT, isHookAllowed, requiredContextKeys } from "./contract.js";