@ttoss/components 2.14.31 → 2.16.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
@@ -14,6 +14,52 @@ pnpm add @ttoss/components @ttoss/ui @emotion/react @ttoss/react-hooks
14
14
 
15
15
  All components are theme-aware and integrate seamlessly with `@ttoss/ui`.
16
16
 
17
+ ### OAuthConsent
18
+
19
+ OAuth consent screen with flat and GitHub-style hierarchical scopes. Granting a parent scope automatically locks all descendants. All visible copy is injected via the `labels` prop — no hardcoded strings. [Docs](https://storybook.ttoss.dev/?path=/docs/components-oauthconsent--docs)
20
+
21
+ ```tsx
22
+ import { OAuthConsent } from '@ttoss/components';
23
+
24
+ <OAuthConsent
25
+ clientName="My App"
26
+ scopes={[
27
+ {
28
+ key: 'openid',
29
+ label: 'OpenID Connect',
30
+ description: 'Verify your identity',
31
+ required: true,
32
+ },
33
+ {
34
+ key: 'profile',
35
+ label: 'Profile',
36
+ description: 'Read your public profile information',
37
+ },
38
+ ]}
39
+ onAuthorize={async (grantedScopes) => {
40
+ const res = await authorize({ scopes: grantedScopes });
41
+ return { ok: res.ok };
42
+ }}
43
+ onAuthorized={() => {
44
+ window.location.href = resumeUrl;
45
+ }}
46
+ onDeny={() => navigate('/')}
47
+ labels={{
48
+ title: 'Authorize access',
49
+ requestedBy: (name) => (
50
+ <>
51
+ <strong>{name}</strong> is requesting access.
52
+ </>
53
+ ),
54
+ permissionsHeading: 'Requested permissions',
55
+ approve: 'Authorize',
56
+ deny: 'Deny',
57
+ invalidRequestTitle: 'Invalid request',
58
+ invalidRequestBody: 'Missing required OAuth parameters. Please try again.',
59
+ }}
60
+ />;
61
+ ```
62
+
17
63
  ### Accordion
18
64
 
19
65
  Accessible accordion component with collapsible content sections. [Docs](https://storybook.ttoss.dev/?path=/docs/components-accordion--docs)
package/dist/index.d.mts CHANGED
@@ -791,11 +791,30 @@ declare const NavList: {
791
791
  displayName: string;
792
792
  };
793
793
  //#endregion
794
+ //#region src/components/NotificationCard/NotificationButton.d.ts
795
+ type NotificationType$1 = 'success' | 'error' | 'warning' | 'info' | 'neutral';
796
+ /** Props for NotificationButton */
797
+ type NotificationButtonProps = Omit<ButtonProps, 'type'> & {
798
+ /** Notification type — controls background and border colors to match the card. */type: NotificationType$1;
799
+ };
800
+ /**
801
+ * A button styled to match the NotificationCard of a given type.
802
+ * Use this for action buttons rendered inside or alongside notification cards.
803
+ */
804
+ declare const NotificationButton: ({
805
+ type,
806
+ ...props
807
+ }: NotificationButtonProps) => import("react/jsx-runtime").JSX.Element;
808
+ //#endregion
794
809
  //#region src/components/NotificationCard/NotificationCard.d.ts
795
810
  type NotificationType = 'success' | 'error' | 'warning' | 'info' | 'neutral';
796
811
  type NotificationAction = {
797
- action?: 'open_url';
798
- url?: string;
812
+ action: 'open_url';
813
+ url: string;
814
+ label?: string;
815
+ } | {
816
+ action: 'callback';
817
+ onClick: () => void;
799
818
  label?: string;
800
819
  };
801
820
  type NotificationCardProps = {
@@ -835,6 +854,119 @@ declare const NotificationsMenu: ({
835
854
  onClearAll
836
855
  }: Props) => import("react/jsx-runtime").JSX.Element;
837
856
  //#endregion
857
+ //#region src/components/OAuthConsent/OAuthConsent.d.ts
858
+ /**
859
+ * A single OAuth scope, optionally with implied child scopes.
860
+ */
861
+ type ConsentScope = {
862
+ /** OAuth scope token, e.g. 'repo', 'write:packages', 'read'. */key: string; /** Bold display name shown on the left. Pre-translated by the consumer. */
863
+ label: string; /** Scope description shown below the label. Pre-translated by the consumer. */
864
+ description: string;
865
+ /**
866
+ * Child scopes implied by this one. When this scope is granted, every
867
+ * descendant is granted and locked (checked + disabled).
868
+ */
869
+ children?: ConsentScope[];
870
+ /**
871
+ * Pre-checked on mount.
872
+ * @default false
873
+ */
874
+ defaultGranted?: boolean;
875
+ /**
876
+ * Always granted; cannot be unchecked. Renders checked + disabled.
877
+ * @default false
878
+ */
879
+ required?: boolean;
880
+ };
881
+ /**
882
+ * All visible copy for the consent screen. Pre-translated by the consumer.
883
+ */
884
+ type OAuthConsentLabels = {
885
+ /** Page heading, e.g. "Authorize access". */title: string; /** Client request line. Receives the client identifier for interpolation. */
886
+ requestedBy: (clientName: string) => React.ReactNode; /** Section heading above the scope list, e.g. "Requested permissions". */
887
+ permissionsHeading: string; /** Approve button label, e.g. "Authorize". */
888
+ approve: string; /** Deny button label, e.g. "Deny". */
889
+ deny: string; /** Heading shown when required OAuth params are missing. */
890
+ invalidRequestTitle: string; /** Body text shown when required OAuth params are missing. */
891
+ invalidRequestBody: string;
892
+ };
893
+ /**
894
+ * Props for the OAuthConsent component.
895
+ */
896
+ type OAuthConsentProps = {
897
+ /** Display name or identifier of the requesting OAuth client. */clientName: string; /** Scope tree to render. */
898
+ scopes: ConsentScope[];
899
+ /**
900
+ * Called when the user approves. Receives the minimal granted scope set
901
+ * (top-most selected keys; implied descendants omitted). Return `{ ok: true }`
902
+ * to signal success; the component then calls `onAuthorized`.
903
+ */
904
+ onAuthorize: (grantedScopes: string[]) => Promise<{
905
+ ok: boolean;
906
+ }>;
907
+ /**
908
+ * Called after a successful `onAuthorize`. Use this to redirect to the OAuth
909
+ * server's resumed /authorize URL. The component does not navigate itself.
910
+ */
911
+ onAuthorized: () => void; /** Called when the user clicks Deny. Use this to navigate away. */
912
+ onDeny: () => void;
913
+ /**
914
+ * True while the authorize call is in flight (disables both buttons).
915
+ * The component also tracks its own internal loading state.
916
+ */
917
+ isAuthorizing?: boolean;
918
+ /**
919
+ * When false, renders the invalid-request error state instead of the form.
920
+ * @default true
921
+ */
922
+ isValidRequest?: boolean; /** All visible copy. Pre-translated by the consumer. */
923
+ labels: OAuthConsentLabels;
924
+ };
925
+ /**
926
+ * Walk the scope tree and return the minimal set of granted scope keys.
927
+ * When a node is selected, its key is emitted and its subtree is skipped
928
+ * (the server expands parent scopes to their children). Nodes not in
929
+ * `selected` are recursed into so that individually-selected children
930
+ * are still captured.
931
+ */
932
+ declare const collectGrantedScopes: (scopes: ConsentScope[], selected: Set<string>) => string[];
933
+ /**
934
+ * Accessible, framework-agnostic OAuth consent screen component.
935
+ *
936
+ * Renders a standards-compliant "authorize this client" page with support for
937
+ * flat and GitHub-style grouped/hierarchical scopes. Granting a parent scope
938
+ * automatically locks all descendant scopes (checked + disabled). All visible
939
+ * copy is injected via `labels`; no strings are hardcoded inside this component.
940
+ *
941
+ * @example
942
+ * ```tsx
943
+ * <OAuthConsent
944
+ * clientName="My App"
945
+ * scopes={[
946
+ * { key: 'read', label: 'Read', description: 'Read access', required: true },
947
+ * { key: 'write', label: 'Write', description: 'Write access' },
948
+ * ]}
949
+ * onAuthorize={async (scopes) => {
950
+ * const res = await authorize({ variables: { scopes } });
951
+ * return { ok: !!res.ok };
952
+ * }}
953
+ * onAuthorized={() => { window.location.href = resumeUrl; }}
954
+ * onDeny={() => navigate('/')}
955
+ * labels={labels}
956
+ * />
957
+ * ```
958
+ */
959
+ declare const OAuthConsent: ({
960
+ clientName,
961
+ scopes,
962
+ onAuthorize,
963
+ onAuthorized,
964
+ onDeny,
965
+ isAuthorizing,
966
+ isValidRequest,
967
+ labels
968
+ }: OAuthConsentProps) => import("react/jsx-runtime").JSX.Element;
969
+ //#endregion
838
970
  //#region src/components/Search/Search.d.ts
839
971
  type SearchProps = Omit<InputProps, 'onChange'> & {
840
972
  loading?: boolean;
@@ -907,4 +1039,4 @@ declare const Tabs: {
907
1039
  declare const ToastContainer: (props: ToastContainerProps) => import("react/jsx-runtime").JSX.Element;
908
1040
  declare const toast: typeof toast$1;
909
1041
  //#endregion
910
- export { Accordion, AccordionItem, AccordionProps, AccordionRenderItemProps, BaseMetric, DateMetric, DatePicker, DateRange, Drawer, DrawerProps, EnhancedTitle, EnhancedTitleBadge, EnhancedTitleProps, EnhancedTitleVariant, FileUploadState, FileUploader, FileUploaderProps, InstallPwa, InstallPwaUi, InstallPwaUiProps, JsonEditor, JsonView, type LinkComponentProps, List, ListItem, ListItemProps, ListProps, LockedOverlay, Markdown, MarkdownProps, Menu, MenuProps, Metric, MetricCard, MetricCardProps, Modal, ModalProps, NavList, type NavListGroup, type NavListItem, type NavListProps, Notification, NotificationAction, NotificationCard, NotificationCardProps, NotificationsMenu, NumberMetric, OnFilesChange, OnRemove, OnUpload, OnUploadComplete, OnUploadError, OnUploadProgress, OnUploadStart, PercentageMetric, Search, SearchProps, SpotlightCard, SpotlightCardProps, type TabListProps, type TabPanelProps, type TabProps, Table, Tabs, type TabsProps, ToastContainer, type ToastContainerProps, type ToastOptions, UploadResult, UploadedFile, createCell, createColumnHelper, flexRender, getCoreRowModel, toast, useReactTable };
1042
+ export { Accordion, AccordionItem, AccordionProps, AccordionRenderItemProps, BaseMetric, ConsentScope, DateMetric, DatePicker, DateRange, Drawer, DrawerProps, EnhancedTitle, EnhancedTitleBadge, EnhancedTitleProps, EnhancedTitleVariant, FileUploadState, FileUploader, FileUploaderProps, InstallPwa, InstallPwaUi, InstallPwaUiProps, JsonEditor, JsonView, type LinkComponentProps, List, ListItem, ListItemProps, ListProps, LockedOverlay, Markdown, MarkdownProps, Menu, MenuProps, Metric, MetricCard, MetricCardProps, Modal, ModalProps, NavList, type NavListGroup, type NavListItem, type NavListProps, Notification, NotificationAction, NotificationButton, NotificationButtonProps, NotificationCard, NotificationCardProps, NotificationsMenu, NumberMetric, OAuthConsent, OAuthConsentLabels, OAuthConsentProps, OnFilesChange, OnRemove, OnUpload, OnUploadComplete, OnUploadError, OnUploadProgress, OnUploadStart, PercentageMetric, Search, SearchProps, SpotlightCard, SpotlightCardProps, type TabListProps, type TabPanelProps, type TabProps, Table, Tabs, type TabsProps, ToastContainer, type ToastContainerProps, type ToastOptions, UploadResult, UploadedFile, collectGrantedScopes, createCell, createColumnHelper, flexRender, getCoreRowModel, toast, useReactTable };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { Badge, BaseStyles, Box, Button, Card, CloseButton, Divider, Flex, Heading, IconButton, Input, Label, Link, Stack, Tag, Text, TooltipIcon, keyframes, useResponsiveValue, useTheme } from "@ttoss/ui";
2
+ import { Badge, BaseStyles, Box, Button, Card, Checkbox, CloseButton, Divider, Flex, Heading, IconButton, Input, Label, Link, Stack, Tag, Text, TooltipIcon, keyframes, useResponsiveValue, useTheme } from "@ttoss/ui";
3
3
  import * as React from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Icon } from "@ttoss/react-icons";
@@ -2510,168 +2510,265 @@ var NavList = ({
2510
2510
  };
2511
2511
  NavList.displayName = "NavList";
2512
2512
 
2513
+ //#endregion
2514
+ //#region src/components/NotificationCard/NotificationButton.tsx
2515
+ var sxMap$1 = {
2516
+ success: {
2517
+ backgroundColor: "feedback.background.positive.active",
2518
+ borderColor: "feedback.border.positive.active"
2519
+ },
2520
+ error: {
2521
+ backgroundColor: "feedback.background.negative.active",
2522
+ borderColor: "feedback.border.negative.active"
2523
+ },
2524
+ warning: {
2525
+ backgroundColor: "feedback.background.caution.active",
2526
+ borderColor: "feedback.border.caution.active"
2527
+ },
2528
+ info: {
2529
+ backgroundColor: "feedback.background.primary.active",
2530
+ borderColor: "feedback.border.primary.active"
2531
+ },
2532
+ neutral: {
2533
+ backgroundColor: "feedback.background.primary.active",
2534
+ borderColor: "feedback.border.primary.active"
2535
+ }
2536
+ };
2537
+ /**
2538
+ * A button styled to match the NotificationCard of a given type.
2539
+ * Use this for action buttons rendered inside or alongside notification cards.
2540
+ */
2541
+ var NotificationButton = ({
2542
+ type,
2543
+ ...props
2544
+ }) => {
2545
+ return /* @__PURE__ */jsx(Button, {
2546
+ type: "button",
2547
+ ...props,
2548
+ sx: {
2549
+ ...sxMap$1[type],
2550
+ color: "display.text.primary.default",
2551
+ borderRadius: "lg",
2552
+ borderWidth: "sm",
2553
+ borderStyle: "solid"
2554
+ }
2555
+ });
2556
+ };
2557
+
2513
2558
  //#endregion
2514
2559
  //#region src/components/NotificationCard/NotificationCard.tsx
2515
- var NotificationCard = props => {
2516
- const sxMap = {
2517
- success: {
2518
- card: {
2519
- backgroundColor: "feedback.background.positive.default",
2520
- borderColor: "feedback.border.positive.default"
2521
- }
2522
- },
2523
- error: {
2524
- card: {
2525
- backgroundColor: "feedback.background.negative.default",
2526
- borderColor: "feedback.border.negative.default"
2527
- }
2528
- },
2529
- warning: {
2530
- card: {
2531
- backgroundColor: "feedback.background.caution.default",
2532
- borderColor: "feedback.border.caution.default"
2533
- }
2534
- },
2535
- info: {
2536
- card: {
2537
- backgroundColor: "feedback.background.primary.default",
2538
- borderColor: "feedback.border.primary.default"
2539
- }
2540
- },
2541
- neutral: {
2542
- card: {
2543
- backgroundColor: "feedback.background.primary.default",
2544
- borderColor: "feedback.border.primary.default"
2545
- }
2560
+ var sxMap = {
2561
+ success: {
2562
+ card: {
2563
+ backgroundColor: "feedback.background.positive.default",
2564
+ borderColor: "feedback.border.positive.default"
2546
2565
  }
2547
- };
2548
- const icon = {
2549
- success: "success-circle",
2550
- error: "error",
2551
- warning: "warning-alt",
2552
- info: "info",
2553
- neutral: "info"
2554
- };
2555
- const hasCaption = Boolean(props.caption);
2556
- const hasActions = Boolean(props.actions && props.actions.length > 0);
2557
- const hasTitle = Boolean(props.title);
2558
- const shouldCenterVerticallyWithoutTitle = !hasCaption && !hasActions && !hasTitle;
2559
- const shouldCenterVerticallyWithTitle = !hasCaption && !hasActions && hasTitle;
2560
- const shouldAddMinHeight = !hasCaption && !hasActions;
2561
- return /* @__PURE__ */jsxs(Card, {
2566
+ },
2567
+ error: {
2568
+ card: {
2569
+ backgroundColor: "feedback.background.negative.default",
2570
+ borderColor: "feedback.border.negative.default"
2571
+ }
2572
+ },
2573
+ warning: {
2574
+ card: {
2575
+ backgroundColor: "feedback.background.caution.default",
2576
+ borderColor: "feedback.border.caution.default"
2577
+ }
2578
+ },
2579
+ info: {
2580
+ card: {
2581
+ backgroundColor: "feedback.background.primary.default",
2582
+ borderColor: "feedback.border.primary.default"
2583
+ }
2584
+ },
2585
+ neutral: {
2586
+ card: {
2587
+ backgroundColor: "feedback.background.primary.default",
2588
+ borderColor: "feedback.border.primary.default"
2589
+ }
2590
+ }
2591
+ };
2592
+ var iconMap = {
2593
+ success: "success-circle",
2594
+ error: "error",
2595
+ warning: "warning-alt",
2596
+ info: "info",
2597
+ neutral: "info"
2598
+ };
2599
+ var NotificationCardTags = ({
2600
+ tags
2601
+ }) => {
2602
+ if (!tags) return null;
2603
+ if (Array.isArray(tags)) {
2604
+ if (tags.length === 0) return null;
2605
+ return /* @__PURE__ */jsx(Fragment, {
2606
+ children: tags.map((tag, index) => {
2607
+ return /* @__PURE__ */jsx(Tag, {
2608
+ children: tag
2609
+ }, index);
2610
+ })
2611
+ });
2612
+ }
2613
+ return /* @__PURE__ */jsx(Tag, {
2614
+ children: tags
2615
+ });
2616
+ };
2617
+ var NotificationCardTitle = ({
2618
+ type,
2619
+ title,
2620
+ tags,
2621
+ onClose
2622
+ }) => {
2623
+ return /* @__PURE__ */jsxs(Card.Title, {
2562
2624
  sx: {
2563
- ...sxMap[props.type].card,
2564
- width: "full"
2625
+ display: "flex",
2626
+ justifyContent: "space-between",
2627
+ alignItems: "center",
2628
+ fontSize: ["md", "xl"],
2629
+ paddingY: ["1", "2", "4"],
2630
+ paddingX: ["2", "4", "8"]
2565
2631
  },
2566
- children: [props.title && /* @__PURE__ */jsxs(Card.Title, {
2632
+ children: [/* @__PURE__ */jsxs(Text, {
2567
2633
  sx: {
2568
- display: "flex",
2569
- justifyContent: "space-between",
2570
- alignItems: "center",
2571
- fontSize: ["md", "xl"],
2572
- paddingY: ["1", "2", "4"],
2573
- paddingX: ["2", "4", "8"]
2634
+ display: "inline-flex",
2635
+ alignItems: "flex-start",
2636
+ gap: "2",
2637
+ flexDirection: "row",
2638
+ flexWrap: "wrap"
2574
2639
  },
2575
- children: [/* @__PURE__ */jsxs(Text, {
2576
- sx: {
2577
- display: "inline-flex",
2578
- alignItems: "flex-start",
2579
- gap: "2",
2580
- flexDirection: "row",
2581
- flexWrap: "wrap"
2582
- },
2583
- children: [/* @__PURE__ */jsxs(Box, {
2584
- sx: {
2585
- display: "flex",
2586
- alignItems: "center",
2587
- gap: "2"
2588
- },
2589
- children: [/* @__PURE__ */jsx(Icon, {
2590
- icon: icon[props.type]
2591
- }), props.title]
2592
- }), props.tags && (Array.isArray(props.tags) ? props.tags.length > 0 : true) && (Array.isArray(props.tags) ? props.tags.map((tag, index) => {
2593
- return /* @__PURE__ */jsx(Tag, {
2594
- children: tag
2595
- }, index);
2596
- }) : /* @__PURE__ */jsx(Tag, {
2597
- children: props.tags
2598
- }))]
2599
- }), props.onClose && /* @__PURE__ */jsx(Box, {
2640
+ children: [/* @__PURE__ */jsxs(Box, {
2600
2641
  sx: {
2601
- marginLeft: "auto"
2642
+ display: "flex",
2643
+ alignItems: "center",
2644
+ gap: "2"
2602
2645
  },
2603
- children: /* @__PURE__ */jsx(CloseButton, {
2604
- onClick: props.onClose
2605
- })
2646
+ children: [/* @__PURE__ */jsx(Icon, {
2647
+ icon: iconMap[type]
2648
+ }), title]
2649
+ }), /* @__PURE__ */jsx(NotificationCardTags, {
2650
+ tags
2606
2651
  })]
2607
- }), /* @__PURE__ */jsxs(Card.Body, {
2652
+ }), onClose && /* @__PURE__ */jsx(Box, {
2653
+ sx: {
2654
+ marginLeft: "auto"
2655
+ },
2656
+ children: /* @__PURE__ */jsx(CloseButton, {
2657
+ onClick: onClose
2658
+ })
2659
+ })]
2660
+ });
2661
+ };
2662
+ var NotificationCardActions = ({
2663
+ type,
2664
+ actions
2665
+ }) => {
2666
+ if (actions.length === 0) return null;
2667
+ return /* @__PURE__ */jsx(Box, {
2668
+ sx: {
2669
+ display: "flex",
2670
+ flexDirection: "row",
2671
+ flexWrap: "wrap",
2672
+ gap: 2,
2673
+ mt: 2
2674
+ },
2675
+ children: actions.map((action, index) => {
2676
+ return /* @__PURE__ */jsx(NotificationButton, {
2677
+ type,
2678
+ onClick: action.action === "open_url" ? () => {
2679
+ return window.open(action.url, "_blank");
2680
+ } : action.onClick,
2681
+ children: action.label || "Acessar"
2682
+ }, index);
2683
+ })
2684
+ });
2685
+ };
2686
+ var NotificationCardBody = ({
2687
+ type,
2688
+ message,
2689
+ actions,
2690
+ caption,
2691
+ onClose,
2692
+ hasTitle
2693
+ }) => {
2694
+ const hasCaption = Boolean(caption);
2695
+ const hasActions = Boolean(actions?.length);
2696
+ const shouldCenter = !hasCaption && !hasActions;
2697
+ const bodyAlign = shouldCenter ? "center" : "flex-start";
2698
+ const bodyMinHeight = shouldCenter && !hasTitle ? "40px" : "auto";
2699
+ return /* @__PURE__ */jsxs(Card.Body, {
2700
+ sx: {
2701
+ ...sxMap[type].card,
2702
+ display: "flex",
2703
+ flexDirection: "column",
2704
+ gap: "2",
2705
+ paddingY: ["1", "2", "4"],
2706
+ paddingX: ["2", "4", "8"]
2707
+ },
2708
+ children: [/* @__PURE__ */jsx(Box, {
2608
2709
  sx: {
2609
- ...sxMap[props.type].card,
2610
2710
  display: "flex",
2611
- flexDirection: "column",
2612
- gap: "2",
2613
- paddingY: ["1", "2", "4"],
2614
- paddingX: ["2", "4", "8"]
2711
+ justifyContent: "space-between",
2712
+ minHeight: bodyMinHeight,
2713
+ alignItems: bodyAlign,
2714
+ width: "100%",
2715
+ gap: "4"
2615
2716
  },
2616
- children: [/* @__PURE__ */jsx(Box, {
2617
- sx: {
2618
- display: "flex",
2619
- justifyContent: "space-between",
2620
- minHeight: shouldAddMinHeight ? "40px" : "auto",
2621
- alignItems: shouldCenterVerticallyWithoutTitle ? "center" : shouldCenterVerticallyWithTitle ? "center" : "flex-start",
2622
- width: "100%",
2623
- gap: "4"
2624
- },
2625
- children: /* @__PURE__ */jsx(Box, {
2626
- sx: {
2627
- flex: 1
2628
- },
2629
- children: /* @__PURE__ */jsx(Text, {
2630
- sx: {
2631
- fontSize: ["sm", "md"]
2632
- },
2633
- children: props.message
2634
- })
2635
- })
2636
- }), props.actions && props.actions.length > 0 && /* @__PURE__ */jsx(Box, {
2637
- sx: {
2638
- display: "flex",
2639
- flexDirection: "column",
2640
- gap: 2,
2641
- mt: 2
2642
- },
2643
- children: props.actions.map((action, index) => {
2644
- return action.action === "open_url" ? /* @__PURE__ */jsx(Link, {
2645
- href: action.url,
2646
- target: "_blank",
2647
- rel: "noopener noreferrer",
2648
- sx: {
2649
- color: "action.text.accent.default",
2650
- cursor: "pointer"
2651
- },
2652
- children: action.label || "Acessar"
2653
- }, index) : null;
2654
- })
2655
- }), props.caption && /* @__PURE__ */jsx(Box, {
2717
+ children: /* @__PURE__ */jsx(Box, {
2656
2718
  sx: {
2657
- whiteSpace: "nowrap",
2658
- mt: 6
2719
+ flex: 1
2659
2720
  },
2660
2721
  children: /* @__PURE__ */jsx(Text, {
2661
2722
  sx: {
2662
- fontSize: "xs",
2663
- color: "text.secondary"
2723
+ fontSize: ["sm", "md"]
2664
2724
  },
2665
- children: props.caption
2725
+ children: message
2666
2726
  })
2667
- }), !props.title && props.onClose && /* @__PURE__ */jsx(Box, {
2727
+ })
2728
+ }), actions && /* @__PURE__ */jsx(NotificationCardActions, {
2729
+ type,
2730
+ actions
2731
+ }), caption && /* @__PURE__ */jsx(Box, {
2732
+ sx: {
2733
+ whiteSpace: "nowrap",
2734
+ mt: 6
2735
+ },
2736
+ children: /* @__PURE__ */jsx(Text, {
2668
2737
  sx: {
2669
- alignSelf: "flex-end"
2738
+ fontSize: "xs",
2739
+ color: "text.secondary"
2670
2740
  },
2671
- children: /* @__PURE__ */jsx(CloseButton, {
2672
- onClick: props.onClose
2673
- })
2674
- })]
2741
+ children: caption
2742
+ })
2743
+ }), !hasTitle && onClose && /* @__PURE__ */jsx(Box, {
2744
+ sx: {
2745
+ alignSelf: "flex-end"
2746
+ },
2747
+ children: /* @__PURE__ */jsx(CloseButton, {
2748
+ onClick: onClose
2749
+ })
2750
+ })]
2751
+ });
2752
+ };
2753
+ var NotificationCard = props => {
2754
+ const hasTitle = Boolean(props.title);
2755
+ return /* @__PURE__ */jsxs(Card, {
2756
+ sx: {
2757
+ ...sxMap[props.type].card,
2758
+ width: "full"
2759
+ },
2760
+ children: [props.title && /* @__PURE__ */jsx(NotificationCardTitle, {
2761
+ type: props.type,
2762
+ title: props.title,
2763
+ tags: props.tags,
2764
+ onClose: props.onClose
2765
+ }), /* @__PURE__ */jsx(NotificationCardBody, {
2766
+ type: props.type,
2767
+ message: props.message,
2768
+ actions: props.actions,
2769
+ caption: props.caption,
2770
+ onClose: props.onClose,
2771
+ hasTitle
2675
2772
  })]
2676
2773
  });
2677
2774
  };
@@ -2883,6 +2980,327 @@ var NotificationsMenu = ({
2883
2980
  });
2884
2981
  };
2885
2982
 
2983
+ //#endregion
2984
+ //#region src/components/OAuthConsent/OAuthConsent.tsx
2985
+ /**
2986
+ * Walk the scope tree and return the minimal set of granted scope keys.
2987
+ * When a node is selected, its key is emitted and its subtree is skipped
2988
+ * (the server expands parent scopes to their children). Nodes not in
2989
+ * `selected` are recursed into so that individually-selected children
2990
+ * are still captured.
2991
+ */
2992
+ var collectGrantedScopes = (scopes, selected) => {
2993
+ const result = [];
2994
+ const walk = nodes => {
2995
+ for (const node of nodes) if (selected.has(node.key)) result.push(node.key);else if (node.children) walk(node.children);
2996
+ };
2997
+ walk(scopes);
2998
+ return result;
2999
+ };
3000
+ var initSelected = scopes => {
3001
+ const selected = /* @__PURE__ */new Set();
3002
+ const walk = nodes => {
3003
+ for (const node of nodes) {
3004
+ if (node.required || node.defaultGranted) selected.add(node.key);
3005
+ if (node.children) walk(node.children);
3006
+ }
3007
+ };
3008
+ walk(scopes);
3009
+ return selected;
3010
+ };
3011
+ var getScopeRowState = ({
3012
+ scope,
3013
+ ancestorSelected,
3014
+ selected,
3015
+ busy
3016
+ }) => {
3017
+ const isChecked = ancestorSelected || selected.has(scope.key);
3018
+ const isLocked = !!scope.required || ancestorSelected;
3019
+ return {
3020
+ isChecked,
3021
+ isDisabled: isLocked || busy,
3022
+ isInteractive: !isLocked && !busy
3023
+ };
3024
+ };
3025
+ var ScopeRow = ({
3026
+ scope,
3027
+ ancestorSelected,
3028
+ selected,
3029
+ busy,
3030
+ onToggle
3031
+ }) => {
3032
+ const {
3033
+ isChecked,
3034
+ isDisabled,
3035
+ isInteractive
3036
+ } = getScopeRowState({
3037
+ scope,
3038
+ ancestorSelected,
3039
+ selected,
3040
+ busy
3041
+ });
3042
+ const checkboxId = `consent-scope-${scope.key}`;
3043
+ const descId = `consent-scope-desc-${scope.key}`;
3044
+ return /* @__PURE__ */jsxs(Box, {
3045
+ as: "li",
3046
+ sx: {
3047
+ listStyle: "none"
3048
+ },
3049
+ children: [/* @__PURE__ */jsxs(Flex, {
3050
+ sx: {
3051
+ gap: 3,
3052
+ alignItems: "flex-start",
3053
+ paddingX: 3,
3054
+ paddingY: 3,
3055
+ borderRadius: "md",
3056
+ opacity: ancestorSelected ? .55 : 1,
3057
+ transition: "background-color 0.15s ease",
3058
+ ...(isInteractive && {
3059
+ "&:hover": {
3060
+ backgroundColor: "display.background.muted.default"
3061
+ }
3062
+ })
3063
+ },
3064
+ children: [/* @__PURE__ */jsx(Box, {
3065
+ sx: {
3066
+ paddingTop: "3px",
3067
+ flexShrink: 0
3068
+ },
3069
+ children: /* @__PURE__ */jsx(Checkbox, {
3070
+ id: checkboxId,
3071
+ checked: isChecked,
3072
+ disabled: isDisabled,
3073
+ "aria-describedby": descId,
3074
+ onChange: () => {
3075
+ onToggle(scope, ancestorSelected);
3076
+ }
3077
+ })
3078
+ }), /* @__PURE__ */jsxs(Box, {
3079
+ sx: {
3080
+ flex: 1,
3081
+ minWidth: 0
3082
+ },
3083
+ children: [/* @__PURE__ */jsx(Label, {
3084
+ htmlFor: checkboxId,
3085
+ sx: {
3086
+ fontWeight: "semibold",
3087
+ fontSize: "md",
3088
+ lineHeight: "short",
3089
+ cursor: isInteractive ? "pointer" : "default",
3090
+ color: "display.text.primary.default",
3091
+ width: "fit-content"
3092
+ },
3093
+ children: scope.label
3094
+ }), /* @__PURE__ */jsx(Text, {
3095
+ id: descId,
3096
+ sx: {
3097
+ fontSize: "sm",
3098
+ lineHeight: "short",
3099
+ color: "display.text.muted.default",
3100
+ display: "block",
3101
+ marginTop: "1"
3102
+ },
3103
+ children: scope.description
3104
+ })]
3105
+ })]
3106
+ }), scope.children?.length ? /* @__PURE__ */jsx(Box, {
3107
+ as: "ul",
3108
+ sx: {
3109
+ listStyle: "none",
3110
+ margin: 0,
3111
+ padding: 0,
3112
+ marginLeft: 5,
3113
+ paddingLeft: 2,
3114
+ borderLeft: "sm",
3115
+ borderColor: "display.border.muted.default"
3116
+ },
3117
+ children: scope.children?.map(child => {
3118
+ return /* @__PURE__ */jsx(ScopeRow, {
3119
+ scope: child,
3120
+ ancestorSelected: isChecked,
3121
+ selected,
3122
+ busy,
3123
+ onToggle
3124
+ }, child.key);
3125
+ })
3126
+ }) : null]
3127
+ });
3128
+ };
3129
+ /** Shared centered card shell for the consent and invalid-request views. */
3130
+ var ConsentCard = ({
3131
+ children
3132
+ }) => {
3133
+ return /* @__PURE__ */jsx(Flex, {
3134
+ sx: {
3135
+ justifyContent: "center",
3136
+ paddingX: 4,
3137
+ paddingY: 6
3138
+ },
3139
+ children: /* @__PURE__ */jsx(Card, {
3140
+ sx: {
3141
+ width: "100%",
3142
+ maxWidth: 480,
3143
+ alignItems: "stretch",
3144
+ padding: 6
3145
+ },
3146
+ children
3147
+ })
3148
+ });
3149
+ };
3150
+ /**
3151
+ * Accessible, framework-agnostic OAuth consent screen component.
3152
+ *
3153
+ * Renders a standards-compliant "authorize this client" page with support for
3154
+ * flat and GitHub-style grouped/hierarchical scopes. Granting a parent scope
3155
+ * automatically locks all descendant scopes (checked + disabled). All visible
3156
+ * copy is injected via `labels`; no strings are hardcoded inside this component.
3157
+ *
3158
+ * @example
3159
+ * ```tsx
3160
+ * <OAuthConsent
3161
+ * clientName="My App"
3162
+ * scopes={[
3163
+ * { key: 'read', label: 'Read', description: 'Read access', required: true },
3164
+ * { key: 'write', label: 'Write', description: 'Write access' },
3165
+ * ]}
3166
+ * onAuthorize={async (scopes) => {
3167
+ * const res = await authorize({ variables: { scopes } });
3168
+ * return { ok: !!res.ok };
3169
+ * }}
3170
+ * onAuthorized={() => { window.location.href = resumeUrl; }}
3171
+ * onDeny={() => navigate('/')}
3172
+ * labels={labels}
3173
+ * />
3174
+ * ```
3175
+ */
3176
+ var OAuthConsent = ({
3177
+ clientName,
3178
+ scopes,
3179
+ onAuthorize,
3180
+ onAuthorized,
3181
+ onDeny,
3182
+ isAuthorizing = false,
3183
+ isValidRequest = true,
3184
+ labels
3185
+ }) => {
3186
+ const [selected, setSelected] = React.useState(() => {
3187
+ return initSelected(scopes);
3188
+ });
3189
+ const [isLoading, setIsLoading] = React.useState(false);
3190
+ const busy = isAuthorizing || isLoading;
3191
+ const handleToggle = React.useCallback((scope, ancestorSelected) => {
3192
+ if (scope.required || ancestorSelected) return;
3193
+ setSelected(prev => {
3194
+ const next = new Set(prev);
3195
+ if (next.has(scope.key)) next.delete(scope.key);else next.add(scope.key);
3196
+ return next;
3197
+ });
3198
+ }, []);
3199
+ const handleApprove = React.useCallback(async () => {
3200
+ const grantedScopes = collectGrantedScopes(scopes, selected);
3201
+ setIsLoading(true);
3202
+ try {
3203
+ if ((await onAuthorize(grantedScopes)).ok) onAuthorized();
3204
+ } finally {
3205
+ setIsLoading(false);
3206
+ }
3207
+ }, [scopes, selected, onAuthorize, onAuthorized]);
3208
+ if (!isValidRequest) return /* @__PURE__ */jsxs(ConsentCard, {
3209
+ children: [/* @__PURE__ */jsx(Heading, {
3210
+ as: "h1",
3211
+ sx: {
3212
+ fontSize: "3xl",
3213
+ color: "display.text.primary.default",
3214
+ marginBottom: 3
3215
+ },
3216
+ children: labels.invalidRequestTitle
3217
+ }), /* @__PURE__ */jsx(Text, {
3218
+ sx: {
3219
+ fontSize: "md",
3220
+ lineHeight: "moderate",
3221
+ color: "display.text.secondary.default"
3222
+ },
3223
+ children: labels.invalidRequestBody
3224
+ })]
3225
+ });
3226
+ return /* @__PURE__ */jsxs(ConsentCard, {
3227
+ children: [/* @__PURE__ */jsx(Heading, {
3228
+ as: "h1",
3229
+ sx: {
3230
+ fontSize: "3xl",
3231
+ lineHeight: "shorter",
3232
+ color: "display.text.primary.default",
3233
+ marginBottom: 2
3234
+ },
3235
+ children: labels.title
3236
+ }), /* @__PURE__ */jsx(Text, {
3237
+ sx: {
3238
+ fontSize: "md",
3239
+ lineHeight: "moderate",
3240
+ color: "display.text.secondary.default"
3241
+ },
3242
+ children: labels.requestedBy(clientName)
3243
+ }), /* @__PURE__ */jsx(Divider, {
3244
+ sx: {
3245
+ marginY: 5,
3246
+ borderColor: "display.border.muted.default"
3247
+ }
3248
+ }), /* @__PURE__ */jsx(Heading, {
3249
+ as: "h3",
3250
+ sx: {
3251
+ fontSize: "xs",
3252
+ fontWeight: "semibold",
3253
+ letterSpacing: "wider",
3254
+ textTransform: "uppercase",
3255
+ color: "display.text.muted.default",
3256
+ marginBottom: 2
3257
+ },
3258
+ children: labels.permissionsHeading
3259
+ }), /* @__PURE__ */jsx(Box, {
3260
+ as: "ul",
3261
+ sx: {
3262
+ listStyle: "none",
3263
+ margin: 0,
3264
+ padding: 0
3265
+ },
3266
+ children: scopes.map(scope => {
3267
+ return /* @__PURE__ */jsx(ScopeRow, {
3268
+ scope,
3269
+ ancestorSelected: false,
3270
+ selected,
3271
+ busy,
3272
+ onToggle: handleToggle
3273
+ }, scope.key);
3274
+ })
3275
+ }), /* @__PURE__ */jsxs(Flex, {
3276
+ sx: {
3277
+ gap: 3,
3278
+ marginTop: 6
3279
+ },
3280
+ children: [/* @__PURE__ */jsx(Button, {
3281
+ variant: "secondary",
3282
+ onClick: onDeny,
3283
+ disabled: busy,
3284
+ sx: {
3285
+ flex: 1,
3286
+ justifyContent: "center"
3287
+ },
3288
+ children: labels.deny
3289
+ }), /* @__PURE__ */jsx(Button, {
3290
+ variant: "accent",
3291
+ onClick: handleApprove,
3292
+ disabled: busy,
3293
+ loading: isLoading || isAuthorizing,
3294
+ sx: {
3295
+ flex: 1,
3296
+ justifyContent: "center"
3297
+ },
3298
+ children: labels.approve
3299
+ })]
3300
+ })]
3301
+ });
3302
+ };
3303
+
2886
3304
  //#endregion
2887
3305
  //#region src/components/Search/Search.tsx
2888
3306
  var Search = ({
@@ -3081,4 +3499,4 @@ var ToastContainer = props => {
3081
3499
  var toast = toast$1;
3082
3500
 
3083
3501
  //#endregion
3084
- export { Accordion, DatePicker, Drawer, EnhancedTitle, FileUploader, InstallPwa, InstallPwaUi, JsonEditor, JsonView, List, ListItem, LockedOverlay, Markdown, Menu, MetricCard, Modal, NavList, NotificationCard, NotificationsMenu, Search, SpotlightCard, Table, Tabs, ToastContainer, createCell, createColumnHelper, flexRender, getCoreRowModel, toast, useReactTable };
3502
+ export { Accordion, DatePicker, Drawer, EnhancedTitle, FileUploader, InstallPwa, InstallPwaUi, JsonEditor, JsonView, List, ListItem, LockedOverlay, Markdown, Menu, MetricCard, Modal, NavList, NotificationButton, NotificationCard, NotificationsMenu, OAuthConsent, Search, SpotlightCard, Table, Tabs, ToastContainer, collectGrantedScopes, createCell, createColumnHelper, flexRender, getCoreRowModel, toast, useReactTable };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/components",
3
- "version": "2.14.31",
3
+ "version": "2.16.0",
4
4
  "description": "React components for ttoss ecosystem.",
5
5
  "keywords": [
6
6
  "React",
@@ -54,19 +54,19 @@
54
54
  "tsdown": "^0.22.2",
55
55
  "tsx": "^4.21.0",
56
56
  "@ttoss/config": "^1.37.17",
57
- "@ttoss/i18n-cli": "^0.8.17",
58
- "@ttoss/react-i18n": "^2.2.21",
59
57
  "@ttoss/react-hooks": "^2.3.20",
60
58
  "@ttoss/react-icons": "^0.7.21",
61
- "@ttoss/ui": "^6.9.27",
62
- "@ttoss/test-utils": "^4.2.18"
59
+ "@ttoss/react-i18n": "^2.3.0",
60
+ "@ttoss/test-utils": "^4.2.18",
61
+ "@ttoss/i18n-cli": "^0.8.17",
62
+ "@ttoss/ui": "^6.9.28"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "react": ">=16.8.0",
66
66
  "@ttoss/react-hooks": "^2.3.20",
67
+ "@ttoss/react-i18n": "^2.3.0",
67
68
  "@ttoss/react-icons": "^0.7.21",
68
- "@ttoss/react-i18n": "^2.2.21",
69
- "@ttoss/ui": "^6.9.27"
69
+ "@ttoss/ui": "^6.9.28"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public",