@seedgrid/fe-components 2026.11.1 → 2026.11.3
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/ai/seedgrid-components.manifest.json +1527 -89
- package/dist/buttons/SgButton.d.ts +6 -2
- package/dist/buttons/SgButton.d.ts.map +1 -1
- package/dist/buttons/SgButton.js +2 -2
- package/dist/buttons/SgButton.meta.js +3 -3
- package/dist/buttons/SgFloatActionButton.d.ts +2 -2
- package/dist/buttons/SgFloatActionButton.d.ts.map +1 -1
- package/dist/buttons/SgFloatActionButton.js +7 -7
- package/dist/buttons/SgFloatActionButton.meta.js +2 -2
- package/dist/buttons/SgSplitButton.d.ts +2 -2
- package/dist/buttons/SgSplitButton.d.ts.map +1 -1
- package/dist/buttons/SgSplitButton.js +4 -4
- package/dist/buttons/SgSplitButton.meta.js +3 -3
- package/dist/commons/SgAvatar.d.ts +2 -2
- package/dist/commons/SgAvatar.d.ts.map +1 -1
- package/dist/commons/SgAvatar.js +3 -3
- package/dist/commons/SgBadge.d.ts +1 -1
- package/dist/commons/SgBadge.d.ts.map +1 -1
- package/dist/commons/SgBadge.js +3 -3
- package/dist/generated/compiledStyles.d.ts +1 -1
- package/dist/generated/compiledStyles.d.ts.map +1 -1
- package/dist/generated/compiledStyles.js +1 -1
- package/dist/i18n/en-US.d.ts.map +1 -1
- package/dist/i18n/en-US.js +55 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +55 -0
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +55 -0
- package/dist/i18n/pt-BR.d.ts.map +1 -1
- package/dist/i18n/pt-BR.js +55 -0
- package/dist/i18n/pt-PT.d.ts.map +1 -1
- package/dist/i18n/pt-PT.js +55 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/inputs/SgAutocomplete.d.ts +1 -1
- package/dist/inputs/SgAutocomplete.d.ts.map +1 -1
- package/dist/inputs/SgAutocomplete.js +3 -3
- package/dist/inputs/SgCameraCapture.d.ts +25 -0
- package/dist/inputs/SgCameraCapture.d.ts.map +1 -0
- package/dist/inputs/SgCameraCapture.js +114 -0
- package/dist/inputs/SgCameraCapture.meta.d.ts +4 -0
- package/dist/inputs/SgCameraCapture.meta.d.ts.map +1 -0
- package/dist/inputs/SgCameraCapture.meta.js +63 -0
- package/dist/inputs/SgCheckboxGroup.d.ts +2 -2
- package/dist/inputs/SgCheckboxGroup.d.ts.map +1 -1
- package/dist/inputs/SgCheckboxGroup.js +12 -12
- package/dist/inputs/SgCombobox.js +5 -5
- package/dist/inputs/SgDatatable.js +11 -11
- package/dist/inputs/SgDatatable.meta.d.ts.map +1 -1
- package/dist/inputs/SgDatatable.meta.js +4 -1
- package/dist/inputs/SgFileUpload.d.ts +69 -0
- package/dist/inputs/SgFileUpload.d.ts.map +1 -0
- package/dist/inputs/SgFileUpload.js +234 -0
- package/dist/inputs/SgFileUpload.meta.d.ts +4 -0
- package/dist/inputs/SgFileUpload.meta.d.ts.map +1 -0
- package/dist/inputs/SgFileUpload.meta.js +71 -0
- package/dist/inputs/SgImageUpload.d.ts +20 -0
- package/dist/inputs/SgImageUpload.d.ts.map +1 -0
- package/dist/inputs/SgImageUpload.js +74 -0
- package/dist/inputs/SgImageUpload.meta.d.ts +4 -0
- package/dist/inputs/SgImageUpload.meta.d.ts.map +1 -0
- package/dist/inputs/SgImageUpload.meta.js +73 -0
- package/dist/inputs/SgInputCurrency.d.ts.map +1 -1
- package/dist/inputs/SgInputCurrency.js +6 -4
- package/dist/inputs/SgInputDate.d.ts.map +1 -1
- package/dist/inputs/SgInputDate.js +3 -4
- package/dist/inputs/SgInputDateTime.d.ts +29 -0
- package/dist/inputs/SgInputDateTime.d.ts.map +1 -0
- package/dist/inputs/SgInputDateTime.js +141 -0
- package/dist/inputs/SgInputDateTime.meta.d.ts +4 -0
- package/dist/inputs/SgInputDateTime.meta.d.ts.map +1 -0
- package/dist/inputs/SgInputDateTime.meta.js +73 -0
- package/dist/inputs/SgInputNumber.d.ts.map +1 -1
- package/dist/inputs/SgInputNumber.js +6 -4
- package/dist/inputs/SgInputText.d.ts.map +1 -1
- package/dist/inputs/SgInputText.js +6 -4
- package/dist/inputs/SgInputTextArea.d.ts.map +1 -1
- package/dist/inputs/SgInputTextArea.js +4 -2
- package/dist/inputs/SgMultiSelect.meta.js +1 -1
- package/dist/inputs/SgMultiSelectChips.meta.js +1 -1
- package/dist/inputs/SgMultiSelectDropdown.js +1 -1
- package/dist/inputs/SgOrderList.d.ts +3 -3
- package/dist/inputs/SgOrderList.d.ts.map +1 -1
- package/dist/inputs/SgOrderList.js +13 -13
- package/dist/inputs/SgOrderList.meta.js +1 -1
- package/dist/inputs/SgPeriodSelector.d.ts +1 -1
- package/dist/inputs/SgPeriodSelector.d.ts.map +1 -1
- package/dist/inputs/SgPeriodSelector.js +3 -3
- package/dist/inputs/SgPickList.d.ts +3 -3
- package/dist/inputs/SgPickList.d.ts.map +1 -1
- package/dist/inputs/SgPickList.js +28 -28
- package/dist/inputs/SgPickList.meta.js +1 -1
- package/dist/inputs/SgRadioGroup.d.ts +2 -2
- package/dist/inputs/SgRadioGroup.d.ts.map +1 -1
- package/dist/inputs/SgRadioGroup.js +5 -5
- package/dist/inputs/SgRating.d.ts +1 -1
- package/dist/inputs/SgRating.d.ts.map +1 -1
- package/dist/inputs/SgRating.js +7 -7
- package/dist/inputs/SgRating.meta.js +1 -1
- package/dist/inputs/SgSlider.d.ts +1 -1
- package/dist/inputs/SgSlider.d.ts.map +1 -1
- package/dist/inputs/SgSlider.js +3 -3
- package/dist/inputs/SgSlider.meta.js +1 -1
- package/dist/inputs/SgStepperInput.d.ts +2 -2
- package/dist/inputs/SgStepperInput.d.ts.map +1 -1
- package/dist/inputs/SgStepperInput.js +4 -4
- package/dist/inputs/SgStepperInput.meta.js +1 -1
- package/dist/inputs/SgTextEditor.d.ts +1 -1
- package/dist/inputs/SgTextEditor.d.ts.map +1 -1
- package/dist/inputs/SgTextEditor.js +14 -14
- package/dist/inputs/SgToggleSwitch.meta.js +1 -1
- package/dist/inputs/SgYearPartSelector.d.ts +1 -1
- package/dist/inputs/SgYearPartSelector.d.ts.map +1 -1
- package/dist/inputs/SgYearPartSelector.js +1 -2
- package/dist/inputs/SgYearPartSelector.meta.js +2 -2
- package/dist/inputs/cameraLogic.d.ts +27 -0
- package/dist/inputs/cameraLogic.d.ts.map +1 -0
- package/dist/inputs/cameraLogic.js +57 -0
- package/dist/inputs/dateTimeLogic.d.ts +28 -0
- package/dist/inputs/dateTimeLogic.d.ts.map +1 -0
- package/dist/inputs/dateTimeLogic.js +153 -0
- package/dist/inputs/fileUploadLogic.d.ts +62 -0
- package/dist/inputs/fileUploadLogic.d.ts.map +1 -0
- package/dist/inputs/fileUploadLogic.js +254 -0
- package/dist/inputs/imageUploadLogic.d.ts +31 -0
- package/dist/inputs/imageUploadLogic.d.ts.map +1 -0
- package/dist/inputs/imageUploadLogic.js +70 -0
- package/dist/inputs/useSgMultiSelect.d.ts +1 -1
- package/dist/inputs/useSgMultiSelect.d.ts.map +1 -1
- package/dist/inputs/useSgMultiSelect.js +1 -1
- package/dist/layout/SgAccordion.d.ts +1 -1
- package/dist/layout/SgAccordion.d.ts.map +1 -1
- package/dist/layout/SgAccordion.js +3 -3
- package/dist/layout/SgBreadcrumb.d.ts +1 -1
- package/dist/layout/SgBreadcrumb.d.ts.map +1 -1
- package/dist/layout/SgBreadcrumb.js +1 -1
- package/dist/layout/SgCard.d.ts +1 -1
- package/dist/layout/SgCard.d.ts.map +1 -1
- package/dist/layout/SgCard.js +14 -14
- package/dist/layout/SgMenu.d.ts +1 -1
- package/dist/layout/SgMenu.d.ts.map +1 -1
- package/dist/layout/SgMenu.js +15 -15
- package/dist/layout/SgPageControl.d.ts +1 -1
- package/dist/layout/SgPageControl.d.ts.map +1 -1
- package/dist/layout/SgPageControl.js +4 -4
- package/dist/layout/SgPageControlPage.meta.js +2 -2
- package/dist/layout/SgToolBar.d.ts +1 -1
- package/dist/layout/SgToolBar.d.ts.map +1 -1
- package/dist/layout/SgToolBar.js +4 -4
- package/dist/layout/SgToolbarIconButton.meta.js +2 -2
- package/dist/layout/SgTreeView.d.ts +4 -4
- package/dist/layout/SgTreeView.d.ts.map +1 -1
- package/dist/layout/SgTreeView.js +6 -6
- package/dist/menus/SgDockMenu.d.ts +1 -1
- package/dist/menus/SgDockMenu.d.ts.map +1 -1
- package/dist/menus/SgDockMenu.js +6 -4
- package/dist/notifications/SgNotificationBell.d.ts +51 -0
- package/dist/notifications/SgNotificationBell.d.ts.map +1 -0
- package/dist/notifications/SgNotificationBell.js +348 -0
- package/dist/notifications/SgNotificationBell.meta.d.ts +4 -0
- package/dist/notifications/SgNotificationBell.meta.d.ts.map +1 -0
- package/dist/notifications/SgNotificationBell.meta.js +75 -0
- package/dist/notifications/notificationBellLogic.d.ts +148 -0
- package/dist/notifications/notificationBellLogic.d.ts.map +1 -0
- package/dist/notifications/notificationBellLogic.js +244 -0
- package/dist/overlay/SgConfirmationDialog.d.ts +1 -1
- package/dist/overlay/SgConfirmationDialog.d.ts.map +1 -1
- package/dist/overlay/SgConfirmationDialog.js +1 -1
- package/dist/overlay/SgPopup.d.ts +1 -1
- package/dist/overlay/SgPopup.d.ts.map +1 -1
- package/dist/overlay/SgPopup.js +1 -1
- package/dist/realtime/SgRealtimeProvider.d.ts +5 -0
- package/dist/realtime/SgRealtimeProvider.d.ts.map +1 -1
- package/dist/realtime/SgRealtimeProvider.js +31 -8
- package/dist/sandbox.cjs +77 -66
- package/dist/style.css +1 -1
- package/package.json +3 -3
|
@@ -12,7 +12,7 @@ export function sgTreeFromJson(json, iconRegistry) {
|
|
|
12
12
|
const walk = (list) => list.map((n) => ({
|
|
13
13
|
id: n.id,
|
|
14
14
|
label: n.label,
|
|
15
|
-
disabled: n.
|
|
15
|
+
disabled: n.enabled === false,
|
|
16
16
|
icon: n.icon ? iconRegistry?.[n.icon] : undefined,
|
|
17
17
|
children: n.children?.length ? walk(n.children) : undefined,
|
|
18
18
|
meta: n.meta
|
|
@@ -27,7 +27,7 @@ export function sgTreeFromJsonWithChecked(json, iconRegistry) {
|
|
|
27
27
|
return {
|
|
28
28
|
id: n.id,
|
|
29
29
|
label: n.label,
|
|
30
|
-
disabled: n.
|
|
30
|
+
disabled: n.enabled === false,
|
|
31
31
|
icon: n.icon ? iconRegistry?.[n.icon] : undefined,
|
|
32
32
|
children: n.children?.length ? walk(n.children) : undefined,
|
|
33
33
|
meta: n.meta
|
|
@@ -309,7 +309,7 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
|
|
|
309
309
|
}
|
|
310
310
|
}, [checkMode, setCheckedIdsLive]);
|
|
311
311
|
const toggleCheck = React.useCallback((node) => {
|
|
312
|
-
if (!checkable || node.
|
|
312
|
+
if (!checkable || (node.enabled === false))
|
|
313
313
|
return;
|
|
314
314
|
const next = new Set(checkedSet);
|
|
315
315
|
const st = computeCheckedState(node.id, maps.childrenById, checkedSet);
|
|
@@ -365,7 +365,7 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
|
|
|
365
365
|
const renderNode = (node, depth) => {
|
|
366
366
|
const hasChildren = !!node.children?.length;
|
|
367
367
|
const isOpen = expandedSet.has(node.id);
|
|
368
|
-
const isDisabled =
|
|
368
|
+
const isDisabled = node.enabled === false;
|
|
369
369
|
const checkedState = checkable ? computeCheckedState(node.id, maps.childrenById, checkedSet) : "unchecked";
|
|
370
370
|
return (_jsxs("div", { children: [_jsxs("div", { className: cn("flex items-center rounded-md", DY.rowPx, DY.rowPy, DY.gap, T.rowHover, isDisabled && "opacity-60"), style: { paddingLeft: 8 + depth * DY.indent }, children: [_jsx("button", { type: "button", className: cn(DY.caret, "shrink-0 rounded-md flex items-center justify-center", "hover:bg-sg-hover2 hover:bg-foreground/10", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", !hasChildren && "opacity-0 pointer-events-none"), onClick: () => hasChildren && !isDisabled && toggleExpand(node), "aria-label": isOpen ? t(i18n, "components.actions.collapse") : t(i18n, "components.actions.expand"), disabled: isDisabled, children: _jsx("span", { className: cn("transition-transform", isOpen && "rotate-90"), children: ">" }) }), checkable &&
|
|
371
371
|
(() => {
|
|
@@ -380,7 +380,7 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
|
|
|
380
380
|
if (track.kind === "custom") {
|
|
381
381
|
return (_jsx("div", { className: cn(TRACK_CELL_CLASS, "flex items-center gap-1"), style: trackCellWidth(track), children: track.render(node) }, track.id));
|
|
382
382
|
}
|
|
383
|
-
const trackDisabled = isDisabled ||
|
|
383
|
+
const trackDisabled = isDisabled || track.enabled?.(node) === false;
|
|
384
384
|
const adornment = track.adornment?.(node) ?? null;
|
|
385
385
|
if (track.kind === "boolean") {
|
|
386
386
|
const checkedSetTrack = data.checkedSet;
|
|
@@ -415,6 +415,6 @@ export const SgTreeView = React.forwardRef(function SgTreeView(props, ref) {
|
|
|
415
415
|
}, disabled: isDisabled, children: node.label })] }), hasChildren && isOpen && _jsx("div", { children: node.children.map((c) => renderNode(c, depth + 1)) })] }, node.id));
|
|
416
416
|
};
|
|
417
417
|
const hasAny = filtered.length > 0;
|
|
418
|
-
return (_jsxs("div", { className: cn("w-full", className), style: style, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [searchable && (_jsx(SgInputText, { id: searchId, labelText: searchPlaceholder, placeholder: searchPlaceholder, prefixIcon: _jsx(Search, { size: 16 }), clearButton: true, filled: true, inputProps: { value: search }, onChange: (value) => setSearch(value) })), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: expandAll, leftIcon: _jsx(ChevronDown, { className: "size-4" }), children: t(i18n, "components.actions.expandAll") }), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: collapseAll, leftIcon: _jsx(ChevronUp, { className: "size-4" }), children: t(i18n, "components.actions.collapseAll") }), checkable && (_jsx(SgButton, { severity: "danger", appearance: "outline", size: "sm", shape: "rounded", onClick: clearChecked, title: t(i18n, "components.actions.clearSelection"), children: t(i18n, "components.actions.clear") }))] }), _jsxs("div", { className: cn("rounded-sg border border-sg-border", T.panelBg, "text-sg-text"), children: [hasTracks && (_jsxs("div", { className: cn("flex items-center border-b border-sg-border", DY.rowPx, DY.rowPy, DY.gap, SZ.text, "font-semibold text-sg-muted"), children: [_jsx("span", { className: cn(DY.caret, "shrink-0") }), checkable && (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), style: trackCellWidth({}), title: props.checkableLabel, children: props.checkableLabel })), (tracks ?? []).map((track) => (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), style: trackCellWidth(track), title: track.label, children: track.label }, track.id)))] })), _jsx("div", { className: cn("overflow-auto", maxHeightClassName), children: _jsx("div", { className: "p-2", children: !hasAny ? (_jsx("div", { className: cn("p-6 text-center", SZ.text, T.subtle), children: emptyText })) : (filtered.map((n) => renderNode(n, 0))) }) }), confirmEnabled && confirmCfg && (_jsx("div", { className: cn("border-t border-sg-border px-3 py-2", (confirmCfg.sticky ?? true) && "sticky bottom-0 bg-sg-surface/95 bg-background/95 backdrop-blur"), children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: cn("text-xs", T.subtle), children: confirmCfg.hint ?? (_jsxs(_Fragment, { children: [t(i18n, "components.tree.selected", { count: confirmSelectedIds.length }), confirmSelection === "leafOnly" ? t(i18n, "components.tree.selectedLeafs") : ""] })) }), _jsxs("div", { className: "flex items-center gap-2", children: [(confirmCfg.showCancel ?? false) && (_jsx("button", { type: "button", onClick: onCancel, className: cn("rounded-md border border-sg-border", T.panelBg, "text-sg-text hover:bg-sg-hover", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", SZ.btn), children: confirmCfg.cancelLabel ?? t(i18n, "components.actions.cancel") })), _jsx(SgButton, { severity: "secondary", appearance: "outline", size: "md", shape: "rounded", onClick: clearChecked,
|
|
418
|
+
return (_jsxs("div", { className: cn("w-full", className), style: style, children: [_jsxs("div", { className: "mb-2 flex items-center gap-2", children: [searchable && (_jsx(SgInputText, { id: searchId, labelText: searchPlaceholder, placeholder: searchPlaceholder, prefixIcon: _jsx(Search, { size: 16 }), clearButton: true, filled: true, inputProps: { value: search }, onChange: (value) => setSearch(value) })), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: expandAll, leftIcon: _jsx(ChevronDown, { className: "size-4" }), children: t(i18n, "components.actions.expandAll") }), _jsx(SgButton, { severity: "secondary", appearance: "ghost", size: "sm", shape: "rounded", onClick: collapseAll, leftIcon: _jsx(ChevronUp, { className: "size-4" }), children: t(i18n, "components.actions.collapseAll") }), checkable && (_jsx(SgButton, { severity: "danger", appearance: "outline", size: "sm", shape: "rounded", onClick: clearChecked, title: t(i18n, "components.actions.clearSelection"), children: t(i18n, "components.actions.clear") }))] }), _jsxs("div", { className: cn("rounded-sg border border-sg-border", T.panelBg, "text-sg-text"), children: [hasTracks && (_jsxs("div", { className: cn("flex items-center border-b border-sg-border", DY.rowPx, DY.rowPy, DY.gap, SZ.text, "font-semibold text-sg-muted"), children: [_jsx("span", { className: cn(DY.caret, "shrink-0") }), checkable && (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), style: trackCellWidth({}), title: props.checkableLabel, children: props.checkableLabel })), (tracks ?? []).map((track) => (_jsx("div", { className: cn(TRACK_CELL_CLASS, "truncate"), style: trackCellWidth(track), title: track.label, children: track.label }, track.id)))] })), _jsx("div", { className: cn("overflow-auto", maxHeightClassName), children: _jsx("div", { className: "p-2", children: !hasAny ? (_jsx("div", { className: cn("p-6 text-center", SZ.text, T.subtle), children: emptyText })) : (filtered.map((n) => renderNode(n, 0))) }) }), confirmEnabled && confirmCfg && (_jsx("div", { className: cn("border-t border-sg-border px-3 py-2", (confirmCfg.sticky ?? true) && "sticky bottom-0 bg-sg-surface/95 bg-background/95 backdrop-blur"), children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: cn("text-xs", T.subtle), children: confirmCfg.hint ?? (_jsxs(_Fragment, { children: [t(i18n, "components.tree.selected", { count: confirmSelectedIds.length }), confirmSelection === "leafOnly" ? t(i18n, "components.tree.selectedLeafs") : ""] })) }), _jsxs("div", { className: "flex items-center gap-2", children: [(confirmCfg.showCancel ?? false) && (_jsx("button", { type: "button", onClick: onCancel, className: cn("rounded-md border border-sg-border", T.panelBg, "text-sg-text hover:bg-sg-hover", "focus:outline-none focus:ring-2 focus:ring-sg-focus/30", SZ.btn), children: confirmCfg.cancelLabel ?? t(i18n, "components.actions.cancel") })), _jsx(SgButton, { severity: "secondary", appearance: "outline", size: "md", shape: "rounded", onClick: clearChecked, enabled: !(!!confirmDisabled), children: t(i18n, "components.actions.clear") }), _jsx(SgButton, { severity: "primary", appearance: "solid", elevation: "sm", size: "md", shape: "rounded", enabled: !(!!confirmDisabled), onClick: onConfirm, children: confirmCfg.label ?? t(i18n, "components.actions.confirm") })] })] }) }))] })] }));
|
|
419
419
|
});
|
|
420
420
|
SgTreeView.displayName = "SgTreeView";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgDockMenu.d.ts","sourceRoot":"","sources":["../../src/menus/SgDockMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GAAG,aAAa,GAAG,aAAa,GAC1C,YAAY,GAAG,eAAe,GAC9B,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,CAAC;AAE9D,MAAM,MAAM,cAAc,GAAG;IAC3B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,sBAAsB;IACtB,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB;IACrB,
|
|
1
|
+
{"version":3,"file":"SgDockMenu.d.ts","sourceRoot":"","sources":["../../src/menus/SgDockMenu.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAQ/B,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GAAG,aAAa,GAAG,aAAa,GAC1C,YAAY,GAAG,eAAe,GAC9B,WAAW,GAAG,cAAc,GAAG,cAAc,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,CAAC;AAE9D,MAAM,MAAM,cAAc,GAAG;IAC3B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,sBAAsB;IACtB,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;IACtB,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,yCAAyC;IACzC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,MAAM,CAAC,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,mBAAmB;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AA0BD,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,2CA2f1D"}
|
package/dist/menus/SgDockMenu.js
CHANGED
|
@@ -265,7 +265,7 @@ export function SgDockMenu(props) {
|
|
|
265
265
|
dragMoved.current = false;
|
|
266
266
|
}, [clearStoredPosition, dragId, enableDragDrop, storageKey]);
|
|
267
267
|
const handleItemClick = React.useCallback((item) => {
|
|
268
|
-
if (item.
|
|
268
|
+
if ((item.enabled === false))
|
|
269
269
|
return;
|
|
270
270
|
if (dragMoved.current) {
|
|
271
271
|
dragMoved.current = false;
|
|
@@ -282,6 +282,8 @@ export function SgDockMenu(props) {
|
|
|
282
282
|
});
|
|
283
283
|
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: containerRef, id: id, className: `sg-dock ${className} ${ELEV_CLS[elevation]}`, style: {
|
|
284
284
|
...containerStyleState.style,
|
|
285
|
+
// A sombra (ELEV_CLS) fica neste wrapper: sem o raio ela desenhava um retangulo quadrado atras do dock (P140).
|
|
286
|
+
borderRadius: `${borderRadius}px`,
|
|
285
287
|
...style,
|
|
286
288
|
transform: containerStyleState.transform,
|
|
287
289
|
cursor: enableDragDrop ? (isDragging ? "grabbing" : "grab") : "default",
|
|
@@ -329,8 +331,8 @@ export function SgDockMenu(props) {
|
|
|
329
331
|
transformOrigin: motion.transformOrigin,
|
|
330
332
|
transition: "transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1)",
|
|
331
333
|
willChange: "transform",
|
|
332
|
-
cursor: item.
|
|
333
|
-
opacity: item.
|
|
334
|
+
cursor: (item.enabled === false) ? "not-allowed" : "pointer",
|
|
335
|
+
opacity: (item.enabled === false) ? 0.5 : 1,
|
|
334
336
|
zIndex: motion.zIndex,
|
|
335
337
|
}, onMouseEnter: () => setHoveredIndex(index), onMouseLeave: () => setHoveredIndex(null), children: [_jsx("button", { type: "button", className: "sg-dock-item-icon focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35", style: {
|
|
336
338
|
width: "100%",
|
|
@@ -346,7 +348,7 @@ export function SgDockMenu(props) {
|
|
|
346
348
|
e.currentTarget.style.backgroundColor = itemSurfaceHoverColor;
|
|
347
349
|
}, onMouseLeave: (e) => {
|
|
348
350
|
e.currentTarget.style.backgroundColor = itemSurfaceColor;
|
|
349
|
-
}, onClick: () => handleItemClick(item), "aria-label": item.label, title: item.label, disabled: item.
|
|
351
|
+
}, onClick: () => handleItemClick(item), "aria-label": item.label, title: item.label, disabled: item.enabled === false, children: item.icon }), item.badge && (_jsx("div", { className: "sg-dock-item-badge", style: {
|
|
350
352
|
position: "absolute",
|
|
351
353
|
top: -4,
|
|
352
354
|
right: -4,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type SgNotification, type SgNotificationPlacement, type SgPollingStats } from "./notificationBellLogic";
|
|
3
|
+
export type { SgNotification, SgNotificationPlacement, SgPollingStats } from "./notificationBellLogic";
|
|
4
|
+
/** Como um tipo de notificacao aparece (icone) e o que faz ao abrir. */
|
|
5
|
+
export type SgNotificationTypeConfig = {
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
onOpen?: (notification: SgNotification) => void;
|
|
8
|
+
};
|
|
9
|
+
export type SgNotificationBellProps = {
|
|
10
|
+
/** Base das rotas da extension `user-notifications`. Default: `/api/user-notifications` (BFF, mesma origem). */
|
|
11
|
+
apiUrl?: string;
|
|
12
|
+
/** Rota que entrega o token do Pulse (usada se nao houver `SgRealtimeProvider` acima). Default: `/api/realtime/connect`. */
|
|
13
|
+
tokenEndpoint?: string;
|
|
14
|
+
/** Opcoes do `fetch` (ex.: `headers: { Authorization }` quando nao ha cookie). */
|
|
15
|
+
requestInit?: RequestInit;
|
|
16
|
+
/** Substitui o `fetch` (apps sem BFF, testes, demonstracao). Default: `fetch` global. */
|
|
17
|
+
fetcher?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
18
|
+
/** Tipo -> icone e acao ao abrir. O sino nao conhece o negocio: relatorio e so um tipo. */
|
|
19
|
+
types?: Record<string, SgNotificationTypeConfig>;
|
|
20
|
+
/** Evento do Pulse que dispara a leitura. Default: `user-notification.*`. */
|
|
21
|
+
eventPattern?: string;
|
|
22
|
+
/** Intervalo do polling de reserva (so com o Pulse desligado ou fora do ar), preso entre 30 e 60 s. Default: 45000. */
|
|
23
|
+
pollIntervalMs?: number;
|
|
24
|
+
/** Maximo de notificacoes na lista. Default e maximo: 100 (uma pagina da rota). */
|
|
25
|
+
maxVisible?: number;
|
|
26
|
+
/** De que lado do sino o painel abre (como o `placement` do SgPopup). `auto` escolhe o lado em que cabe. Default: `auto`. */
|
|
27
|
+
placement?: SgNotificationPlacement;
|
|
28
|
+
/** Mostra o interruptor "Nao lidas" no topo do painel. `false` esconde so o interruptor (o sino fica) e o painel lista tudo. Default: true. */
|
|
29
|
+
showUnreadToggle?: boolean;
|
|
30
|
+
/** O painel abre em "Nao lidas" (ainda nao clicadas). Ignorada com `showUnreadToggle=false`. Nao persiste entre recargas. Default: true. */
|
|
31
|
+
defaultUnreadOnly?: boolean;
|
|
32
|
+
/** Chegou uma notificacao nova (aviso do Pulse). */
|
|
33
|
+
onNotification?: (notification: SgNotification) => void;
|
|
34
|
+
/** O usuario excluiu uma notificacao. */
|
|
35
|
+
onNotificationDelete?: (notification: SgNotification) => void;
|
|
36
|
+
/** O usuario abriu uma notificacao (depois do `onOpen` do tipo). */
|
|
37
|
+
onOpen?: (notification: SgNotification) => void;
|
|
38
|
+
/** O polling consultou a API: contador, instante e motivo. Para medir o custo. */
|
|
39
|
+
onPollingTick?: (stats: SgPollingStats) => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Sino de notificacoes do usuario logado (P129). Autonomo: se nao houver `SgRealtimeProvider` acima, abre a propria
|
|
44
|
+
* conexao com o Pulse (token por `tokenEndpoint`, nunca a apikey no front); com o Pulse desligado ou fora do ar, faz
|
|
45
|
+
* polling de reserva. Sem regra de negocio: o que cada `type` mostra e faz vem de `types`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function SgNotificationBell(props: Readonly<SgNotificationBellProps>): import("react/jsx-runtime").JSX.Element;
|
|
48
|
+
export declare namespace SgNotificationBell {
|
|
49
|
+
var displayName: string;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=SgNotificationBell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SgNotificationBell.d.ts","sourceRoot":"","sources":["../../src/notifications/SgNotificationBell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,EA2BL,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAG5B,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEvG,wEAAwE;AACxE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,cAAc,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,gHAAgH;IAChH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4HAA4H;IAC5H,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnE,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IACjD,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uHAAuH;IACvH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6HAA6H;IAC7H,SAAS,CAAC,EAAE,uBAAuB,CAAC;IACpC,+IAA+I;IAC/I,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4IAA4I;IAC5I,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oDAAoD;IACpD,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,cAAc,KAAK,IAAI,CAAC;IACxD,yCAAyC;IACzC,oBAAoB,CAAC,EAAE,CAAC,YAAY,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9D,oEAAoE;IACpE,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,kFAAkF;IAClF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA8dF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,uBAAuB,CAAC,2CAoB1E;yBApBe,kBAAkB"}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Bell, Trash2 } from "lucide-react";
|
|
5
|
+
import { t, useComponentsI18n } from "../i18n";
|
|
6
|
+
import { SgBadge } from "../commons/SgBadge";
|
|
7
|
+
import { SgToggleSwitch } from "../inputs/SgToggleSwitch";
|
|
8
|
+
import { SgRealtimeProvider, useHasSgRealtimeProvider, useSgRealtimeEvent, useSgRealtimeStatus } from "../realtime/SgRealtimeProvider";
|
|
9
|
+
import { DEFAULT_EVENT_PATTERN, EMPTY_POLLING_STATS, NOTIFICATIONS_CHANNEL, badgeText, clampMaxVisible, afterOpen, displayUnread, initialUnreadOnly, listQuery, visibleItems, isValidInstant, markAllRead, newUnread, nextPollDelay, normalizeList, PANEL_MAX_HEIGHT, PANEL_WIDTH, panelPosition, planEvent, pollingReason, recordPollingTick, relativeTime, removeNotification, shouldRefreshOnStatus, viewState } from "./notificationBellLogic";
|
|
10
|
+
const DEFAULT_API_URL = "/api/user-notifications";
|
|
11
|
+
const PAGE_SIZE = 100;
|
|
12
|
+
function reducedMotion() {
|
|
13
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
14
|
+
}
|
|
15
|
+
function SgNotificationBellInner(props) {
|
|
16
|
+
const { apiUrl = DEFAULT_API_URL, requestInit, types, eventPattern = DEFAULT_EVENT_PATTERN, pollIntervalMs, placement = "auto", showUnreadToggle = true, className } = props;
|
|
17
|
+
const maxVisible = clampMaxVisible(props.maxVisible);
|
|
18
|
+
const i18n = useComponentsI18n();
|
|
19
|
+
const { status } = useSgRealtimeStatus();
|
|
20
|
+
const [items, setItems] = React.useState([]);
|
|
21
|
+
const [total, setTotal] = React.useState(0);
|
|
22
|
+
/** Contador do servidor (GET /unread-count); a lista local pode estar limitada pelo teto. */
|
|
23
|
+
const [serverUnread, setServerUnread] = React.useState(null);
|
|
24
|
+
const [loaded, setLoaded] = React.useState(false);
|
|
25
|
+
const [failed, setFailed] = React.useState(false);
|
|
26
|
+
const [actionFailed, setActionFailed] = React.useState(false);
|
|
27
|
+
const [open, setOpen] = React.useState(false);
|
|
28
|
+
const [unreadOnly, setUnreadOnly] = React.useState(() => initialUnreadOnly(props.showUnreadToggle, props.defaultUnreadOnly));
|
|
29
|
+
const unreadOnlyRef = React.useRef(unreadOnly);
|
|
30
|
+
unreadOnlyRef.current = unreadOnly;
|
|
31
|
+
const panelRef = React.useRef(null);
|
|
32
|
+
const [position, setPosition] = React.useState(null);
|
|
33
|
+
const [announce, setAnnounce] = React.useState("");
|
|
34
|
+
const loadedRef = React.useRef(false);
|
|
35
|
+
const notifiedRef = React.useRef(new Set());
|
|
36
|
+
const itemsRef = React.useRef([]);
|
|
37
|
+
itemsRef.current = items;
|
|
38
|
+
const latest = React.useRef(props);
|
|
39
|
+
latest.current = props;
|
|
40
|
+
const bellRef = React.useRef(null);
|
|
41
|
+
const rootRef = React.useRef(null);
|
|
42
|
+
const base = apiUrl.replace(/\/+$/, "");
|
|
43
|
+
/** ETag fraca + corpo de cada pagina da lista (chave: query). Em 304 vale o corpo guardado. */
|
|
44
|
+
const listCache = React.useRef(new Map());
|
|
45
|
+
const request = React.useCallback(async (path, method, ifNoneMatch) => {
|
|
46
|
+
const init = { credentials: "same-origin", ...latest.current.requestInit, method };
|
|
47
|
+
if (ifNoneMatch) {
|
|
48
|
+
const headers = new Headers(init.headers);
|
|
49
|
+
headers.set("If-None-Match", ifNoneMatch);
|
|
50
|
+
init.headers = headers;
|
|
51
|
+
}
|
|
52
|
+
const response = await (latest.current.fetcher ?? fetch)(base + path, init);
|
|
53
|
+
if (response.status === 304 && ifNoneMatch)
|
|
54
|
+
return response;
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw Object.assign(new Error(`HTTP ${response.status}`), { status: response.status });
|
|
57
|
+
return response;
|
|
58
|
+
}, [base]);
|
|
59
|
+
const ring = React.useCallback(() => {
|
|
60
|
+
const el = bellRef.current;
|
|
61
|
+
if (!el || reducedMotion() || typeof el.animate !== "function")
|
|
62
|
+
return;
|
|
63
|
+
el.animate([{ transform: "rotate(0)" }, { transform: "rotate(14deg)" }, { transform: "rotate(-12deg)" }, { transform: "rotate(8deg)" }, { transform: "rotate(0)" }], { duration: 700, easing: "ease-in-out" });
|
|
64
|
+
}, []);
|
|
65
|
+
/** Le a lista (ate maxVisible, em paginas de 100). `announceNew`: chegou por aviso, entao anima e avisa. */
|
|
66
|
+
const load = React.useCallback(async (opts) => {
|
|
67
|
+
try {
|
|
68
|
+
const only = opts?.unreadOnly ?? unreadOnlyRef.current;
|
|
69
|
+
const wanted = Math.max(1, maxVisible);
|
|
70
|
+
const collected = [];
|
|
71
|
+
let reported = 0;
|
|
72
|
+
for (let page = 0; collected.length < wanted; page += 1) {
|
|
73
|
+
const size = Math.min(PAGE_SIZE, wanted - collected.length);
|
|
74
|
+
const query = listQuery(page, size, only);
|
|
75
|
+
const cached = listCache.current.get(query);
|
|
76
|
+
const response = await request(query, "GET", cached?.etag);
|
|
77
|
+
let res;
|
|
78
|
+
if (response.status === 304 && cached) {
|
|
79
|
+
res = cached.body;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
res = (await response.json());
|
|
83
|
+
const etag = response.headers?.get?.("ETag");
|
|
84
|
+
if (etag)
|
|
85
|
+
listCache.current.set(query, { etag, body: res });
|
|
86
|
+
else
|
|
87
|
+
listCache.current.delete(query);
|
|
88
|
+
}
|
|
89
|
+
collected.push(...res.items);
|
|
90
|
+
reported = res.total;
|
|
91
|
+
if (res.items.length < size || collected.length >= reported)
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
let unreadFromServer = null;
|
|
95
|
+
try {
|
|
96
|
+
const body = (await (await request("/unread-count", "GET")).json());
|
|
97
|
+
if (typeof body.count === "number")
|
|
98
|
+
unreadFromServer = body.count;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// sem o contador do servidor, vale a lista local
|
|
102
|
+
}
|
|
103
|
+
const previous = itemsRef.current;
|
|
104
|
+
const next = normalizeList(collected, maxVisible, new Date().toISOString());
|
|
105
|
+
const fresh = newUnread(previous, next);
|
|
106
|
+
const wasLoaded = loadedRef.current;
|
|
107
|
+
loadedRef.current = true;
|
|
108
|
+
setServerUnread(unreadFromServer);
|
|
109
|
+
setItems(next);
|
|
110
|
+
setTotal(reported);
|
|
111
|
+
setFailed(false);
|
|
112
|
+
setLoaded(true);
|
|
113
|
+
if (opts?.announceNew && wasLoaded && fresh.length > 0) {
|
|
114
|
+
ring();
|
|
115
|
+
setAnnounce(t(i18n, "components.notifications.bell.announce", { title: fresh[0]?.title ?? "" }));
|
|
116
|
+
}
|
|
117
|
+
if (opts?.notifyId) {
|
|
118
|
+
const arrived = next.find((n) => n.publicId === opts.notifyId);
|
|
119
|
+
if (arrived && !notifiedRef.current.has(arrived.publicId) && !previous.some((n) => n.publicId === arrived.publicId)) {
|
|
120
|
+
notifiedRef.current.add(arrived.publicId);
|
|
121
|
+
latest.current.onNotification?.(arrived);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
setFailed(true);
|
|
128
|
+
setLoaded(true);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}, [i18n, maxVisible, request, ring]);
|
|
132
|
+
// Leitura inicial.
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
void load();
|
|
135
|
+
}, [load]);
|
|
136
|
+
// Aviso do Pulse.
|
|
137
|
+
useSgRealtimeEvent(NOTIFICATIONS_CHANNEL, eventPattern, (event) => {
|
|
138
|
+
const plan = planEvent(eventPattern, event.channel, event.event, event.data);
|
|
139
|
+
if (plan.action === "fetch")
|
|
140
|
+
void load({ notifyId: plan.publicId, announceNew: true });
|
|
141
|
+
else if (plan.action === "refresh")
|
|
142
|
+
void load();
|
|
143
|
+
});
|
|
144
|
+
// Ao (re)conectar, le uma vez para recuperar o que passou.
|
|
145
|
+
const previousStatus = React.useRef(status);
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
if (shouldRefreshOnStatus(previousStatus.current, status))
|
|
148
|
+
void load();
|
|
149
|
+
previousStatus.current = status;
|
|
150
|
+
}, [status, load]);
|
|
151
|
+
// Polling de reserva: so com o Pulse desligado ou fora do ar; para com a aba oculta.
|
|
152
|
+
const reason = pollingReason(status);
|
|
153
|
+
const stats = React.useRef(EMPTY_POLLING_STATS);
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
if (!reason || typeof document === "undefined")
|
|
156
|
+
return undefined;
|
|
157
|
+
let timer;
|
|
158
|
+
let failures = 0;
|
|
159
|
+
let stopped = false;
|
|
160
|
+
let inFlight = false;
|
|
161
|
+
const schedule = () => {
|
|
162
|
+
if (timer)
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
timer = undefined;
|
|
165
|
+
if (stopped || document.hidden)
|
|
166
|
+
return;
|
|
167
|
+
timer = setTimeout(() => void tick(), nextPollDelay(pollIntervalMs, failures, Math.random()));
|
|
168
|
+
};
|
|
169
|
+
// Uma consulta por vez: trocar de aba durante uma consulta nao abre um segundo laco.
|
|
170
|
+
const tick = async () => {
|
|
171
|
+
if (inFlight || stopped)
|
|
172
|
+
return;
|
|
173
|
+
inFlight = true;
|
|
174
|
+
if (timer)
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
timer = undefined;
|
|
177
|
+
try {
|
|
178
|
+
stats.current = recordPollingTick(stats.current, new Date().toISOString(), reason);
|
|
179
|
+
latest.current.onPollingTick?.(stats.current);
|
|
180
|
+
failures = (await load({ announceNew: true })) ? 0 : failures + 1;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
inFlight = false;
|
|
184
|
+
}
|
|
185
|
+
schedule();
|
|
186
|
+
};
|
|
187
|
+
const onVisibility = () => {
|
|
188
|
+
if (document.hidden) {
|
|
189
|
+
if (timer)
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
timer = undefined;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
void tick();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
198
|
+
schedule();
|
|
199
|
+
return () => {
|
|
200
|
+
stopped = true;
|
|
201
|
+
if (timer)
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
204
|
+
};
|
|
205
|
+
}, [reason, pollIntervalMs, request, load]);
|
|
206
|
+
// Fecha com Esc e com clique fora.
|
|
207
|
+
React.useEffect(() => {
|
|
208
|
+
if (!open)
|
|
209
|
+
return undefined;
|
|
210
|
+
const onKey = (e) => {
|
|
211
|
+
if (e.key === "Escape") {
|
|
212
|
+
setOpen(false);
|
|
213
|
+
bellRef.current?.focus();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const onDown = (e) => {
|
|
217
|
+
if (rootRef.current && !rootRef.current.contains(e.target))
|
|
218
|
+
setOpen(false);
|
|
219
|
+
};
|
|
220
|
+
document.addEventListener("keydown", onKey);
|
|
221
|
+
document.addEventListener("mousedown", onDown);
|
|
222
|
+
return () => {
|
|
223
|
+
document.removeEventListener("keydown", onKey);
|
|
224
|
+
document.removeEventListener("mousedown", onDown);
|
|
225
|
+
};
|
|
226
|
+
}, [open]);
|
|
227
|
+
/** 404 em ler/excluir: a notificacao ja nao existe no servidor (outra aba, retencao); sai da lista local. */
|
|
228
|
+
const act = async (fn, target) => {
|
|
229
|
+
try {
|
|
230
|
+
await fn();
|
|
231
|
+
setActionFailed(false);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (target && error.status === 404) {
|
|
235
|
+
setItems((list) => removeNotification(list, target.publicId));
|
|
236
|
+
setTotal((v) => Math.max(0, v - 1));
|
|
237
|
+
if (!target.read)
|
|
238
|
+
setServerUnread((v) => (v === null ? v : Math.max(0, v - 1)));
|
|
239
|
+
setActionFailed(false);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
setActionFailed(true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const openItem = (n) => act(async () => {
|
|
247
|
+
if (!n.read) {
|
|
248
|
+
await request(`/${encodeURIComponent(n.publicId)}/read`, "POST");
|
|
249
|
+
setItems((list) => afterOpen(list, n.publicId, unreadOnlyRef.current, new Date().toISOString()));
|
|
250
|
+
setServerUnread((v) => (v === null ? v : Math.max(0, v - 1)));
|
|
251
|
+
}
|
|
252
|
+
setOpen(false);
|
|
253
|
+
types?.[n.type]?.onOpen?.(n);
|
|
254
|
+
latest.current.onOpen?.(n);
|
|
255
|
+
}, n);
|
|
256
|
+
const deleteItem = (n) => act(async () => {
|
|
257
|
+
await request(`/${encodeURIComponent(n.publicId)}`, "DELETE");
|
|
258
|
+
setItems((list) => removeNotification(list, n.publicId));
|
|
259
|
+
setTotal((v) => Math.max(0, v - 1));
|
|
260
|
+
if (!n.read)
|
|
261
|
+
setServerUnread((v) => (v === null ? v : Math.max(0, v - 1)));
|
|
262
|
+
latest.current.onNotificationDelete?.(n);
|
|
263
|
+
}, n);
|
|
264
|
+
const readAll = () => act(async () => {
|
|
265
|
+
await request("/read-all", "POST");
|
|
266
|
+
setItems((list) => markAllRead(list, new Date().toISOString()));
|
|
267
|
+
setServerUnread(0);
|
|
268
|
+
});
|
|
269
|
+
const clearAll = () => act(async () => {
|
|
270
|
+
await request("", "DELETE");
|
|
271
|
+
setItems([]);
|
|
272
|
+
setTotal(0);
|
|
273
|
+
setServerUnread(0);
|
|
274
|
+
});
|
|
275
|
+
const shown = visibleItems(items, unreadOnly);
|
|
276
|
+
const count = displayUnread(serverUnread, items);
|
|
277
|
+
const view = viewState({ loaded, failed, count: shown.length, unreadOnly });
|
|
278
|
+
const changeUnreadOnly = (checked) => {
|
|
279
|
+
setUnreadOnly(checked);
|
|
280
|
+
unreadOnlyRef.current = checked;
|
|
281
|
+
void load({ unreadOnly: checked });
|
|
282
|
+
};
|
|
283
|
+
const now = new Date().toISOString();
|
|
284
|
+
const ageText = (n) => {
|
|
285
|
+
const r = relativeTime(n.createdAt, now);
|
|
286
|
+
if (r.unit === "now")
|
|
287
|
+
return t(i18n, "components.notifications.bell.now");
|
|
288
|
+
if (r.unit === "minute")
|
|
289
|
+
return t(i18n, "components.notifications.bell.minutes", { n: r.value });
|
|
290
|
+
if (r.unit === "hour")
|
|
291
|
+
return t(i18n, "components.notifications.bell.hours", { n: r.value });
|
|
292
|
+
if (r.unit === "day")
|
|
293
|
+
return t(i18n, "components.notifications.bell.days", { n: r.value });
|
|
294
|
+
return isValidInstant(n.createdAt) ? new Date(n.createdAt).toLocaleDateString(i18n.locale) : "";
|
|
295
|
+
};
|
|
296
|
+
const bellLabel = count > 0
|
|
297
|
+
? t(i18n, "components.notifications.bell.unread", { count })
|
|
298
|
+
: t(i18n, "components.notifications.bell.none");
|
|
299
|
+
// Posicao do painel na janela: o lado vem de `placement`; `auto` escolhe onde cabe. Recalcula ao abrir, ao mudar a
|
|
300
|
+
// lista, ao redimensionar e ao rolar.
|
|
301
|
+
React.useLayoutEffect(() => {
|
|
302
|
+
if (!open) {
|
|
303
|
+
setPosition(null);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const place = () => {
|
|
307
|
+
const anchor = bellRef.current?.getBoundingClientRect();
|
|
308
|
+
const panel = panelRef.current;
|
|
309
|
+
if (!anchor || !panel)
|
|
310
|
+
return;
|
|
311
|
+
const height = Math.min(panel.scrollHeight, PANEL_MAX_HEIGHT);
|
|
312
|
+
setPosition(panelPosition(anchor, { width: Math.min(PANEL_WIDTH, panel.offsetWidth || PANEL_WIDTH), height }, { width: window.innerWidth, height: window.innerHeight }, placement));
|
|
313
|
+
};
|
|
314
|
+
place();
|
|
315
|
+
window.addEventListener("resize", place);
|
|
316
|
+
window.addEventListener("scroll", place, true);
|
|
317
|
+
return () => {
|
|
318
|
+
window.removeEventListener("resize", place);
|
|
319
|
+
window.removeEventListener("scroll", place, true);
|
|
320
|
+
};
|
|
321
|
+
}, [open, placement, items.length, loaded, failed]);
|
|
322
|
+
return (_jsxs("div", { ref: rootRef, className: ["relative inline-block", className].filter(Boolean).join(" "), children: [_jsxs("button", { ref: bellRef, type: "button", "aria-label": bellLabel, "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => {
|
|
323
|
+
if (!open)
|
|
324
|
+
void load();
|
|
325
|
+
setOpen((v) => !v);
|
|
326
|
+
}, className: "relative inline-flex size-11 items-center justify-center rounded-full text-foreground hover:bg-muted/60 focus-visible:outline focus-visible:outline-2", children: [_jsx(Bell, { className: "size-5", "aria-hidden": "true" }), count > 0 ? (_jsx("span", { className: "pointer-events-none absolute right-1 top-1", children: _jsx(SgBadge, { value: badgeText(count), severity: "danger", size: "xs", rounded: true }) })) : null] }), _jsx("span", { className: "sr-only", role: "status", "aria-live": "polite", children: announce }), open ? (_jsxs("div", { ref: panelRef, role: "dialog", "aria-label": t(i18n, "components.notifications.bell.label"), "data-side": position?.side, style: { top: position?.top ?? 0, left: position?.left ?? 0, maxHeight: position?.maxHeight ?? PANEL_MAX_HEIGHT, visibility: position ? "visible" : "hidden" }, className: "fixed z-50 flex w-[min(26rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-xl border border-border bg-background shadow-lg", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border px-4 py-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-base font-semibold", children: t(i18n, "components.notifications.bell.label") }), count > 0 ? (_jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-semibold", children: t(i18n, "components.notifications.bell.news", { count }) })) : null] }), count > 0 ? (_jsx("button", { type: "button", onClick: () => void readAll(), className: "min-h-11 px-1 text-sm font-medium text-primary", children: t(i18n, "components.notifications.bell.markAll") })) : null] }), showUnreadToggle ? (_jsx("div", { className: "flex items-center border-b border-border px-4 py-1", children: _jsx(SgToggleSwitch, { id: "sg-notification-unread-only", label: t(i18n, "components.notifications.bell.unreadOnly"), checked: unreadOnly, onChange: changeUnreadOnly }) })) : null, actionFailed ? (_jsx("div", { role: "alert", className: "border-b border-border px-4 py-2 text-sm text-destructive", children: t(i18n, "components.notifications.bell.actionFailed") })) : null, _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [view === "loading" ? (_jsx("div", { role: "status", className: "px-4 py-6 text-center text-sm text-muted-foreground", children: t(i18n, "components.notifications.bell.loading") })) : null, view === "error" ? (_jsxs("div", { role: "alert", className: "flex flex-col items-center gap-2 px-4 py-6 text-center text-sm", children: [_jsx("span", { children: t(i18n, "components.notifications.bell.error") }), _jsx("button", { type: "button", onClick: () => void load(), className: "min-h-11 px-3 font-medium text-primary", children: t(i18n, "components.notifications.bell.retry") })] })) : null, view === "empty" ? (_jsxs("div", { className: "px-4 py-8 text-center", children: [_jsx("div", { className: "text-sm font-medium", children: t(i18n, "components.notifications.bell.empty") }), _jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: t(i18n, "components.notifications.bell.emptyHint") })] })) : null, view === "caughtUp" ? (_jsxs("div", { className: "px-4 py-8 text-center", children: [_jsx("div", { className: "text-sm font-medium", children: t(i18n, "components.notifications.bell.caughtUp") }), _jsx("button", { type: "button", onClick: () => changeUnreadOnly(false), className: "mt-1 min-h-11 px-3 text-sm font-medium text-primary", children: t(i18n, "components.notifications.bell.seeAll") })] })) : null, view === "list" ? (_jsx("ul", { className: "m-0 list-none p-0", children: shown.map((n) => (_jsxs("li", { "data-unread": n.read ? "false" : "true", className: ["flex items-start gap-3 border-b border-border px-4 py-3", n.read ? "" : "bg-muted/40"].join(" "), children: [_jsx("span", { className: "mt-0.5 inline-flex size-9 shrink-0 items-center justify-center rounded-full border border-border bg-background", children: types?.[n.type]?.icon ?? _jsx(Bell, { className: "size-4", "aria-hidden": "true" }) }), _jsxs("button", { type: "button", onClick: () => void openItem(n), className: "min-w-0 flex-1 text-left", children: [_jsx("span", { className: ["block truncate text-sm", n.read ? "font-medium" : "font-semibold"].join(" "), children: n.title }), n.message ? _jsx("span", { className: "block text-sm text-muted-foreground", children: n.message }) : null, _jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: ageText(n) })] }), _jsx("button", { type: "button", "aria-label": t(i18n, "components.notifications.bell.delete", { title: n.title }), onClick: () => void deleteItem(n), className: "-mr-2 inline-flex size-11 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground", children: _jsx(Trash2, { className: "size-4", "aria-hidden": "true" }) })] }, n.publicId))) })) : null] }), view === "list" ? (_jsxs("div", { className: "flex items-center justify-between border-t border-border px-4", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: t(i18n, "components.notifications.bell.footer", { shown: shown.length, total: Math.max(total, shown.length) }) }), _jsx("button", { type: "button", onClick: () => void clearAll(), className: "min-h-11 px-1 text-sm font-medium text-destructive", children: t(i18n, "components.notifications.bell.clear") })] })) : null] })) : null] }));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Sino de notificacoes do usuario logado (P129). Autonomo: se nao houver `SgRealtimeProvider` acima, abre a propria
|
|
330
|
+
* conexao com o Pulse (token por `tokenEndpoint`, nunca a apikey no front); com o Pulse desligado ou fora do ar, faz
|
|
331
|
+
* polling de reserva. Sem regra de negocio: o que cada `type` mostra e faz vem de `types`.
|
|
332
|
+
*/
|
|
333
|
+
export function SgNotificationBell(props) {
|
|
334
|
+
const hasProvider = useHasSgRealtimeProvider(NOTIFICATIONS_CHANNEL);
|
|
335
|
+
const { fetcher, tokenEndpoint = "/api/realtime/connect", requestInit } = props;
|
|
336
|
+
const fetchToken = React.useMemo(() => fetcher
|
|
337
|
+
? async () => {
|
|
338
|
+
const response = await fetcher(tokenEndpoint, { credentials: "same-origin", ...requestInit, method: "POST" });
|
|
339
|
+
if (!response.ok)
|
|
340
|
+
throw new Error(`HTTP ${response.status}`);
|
|
341
|
+
return (await response.json());
|
|
342
|
+
}
|
|
343
|
+
: undefined, [fetcher, tokenEndpoint, requestInit]);
|
|
344
|
+
if (hasProvider)
|
|
345
|
+
return _jsx(SgNotificationBellInner, { ...props });
|
|
346
|
+
return (_jsx(SgRealtimeProvider, { channels: [NOTIFICATIONS_CHANNEL], tokenEndpoint: tokenEndpoint, tokenRequestInit: requestInit, fetchToken: fetchToken, children: _jsx(SgNotificationBellInner, { ...props }) }));
|
|
347
|
+
}
|
|
348
|
+
SgNotificationBell.displayName = "SgNotificationBell";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SgNotificationBell.meta.d.ts","sourceRoot":"","sources":["../../src/notifications/SgNotificationBell.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,QAsDpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAoBrB,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const sgMeta = {
|
|
2
|
+
version: "0.1",
|
|
3
|
+
componentId: "feedback.notification-bell",
|
|
4
|
+
package: "@seedgrid/fe-components",
|
|
5
|
+
exportName: "SgNotificationBell",
|
|
6
|
+
slug: "sg-notification-bell",
|
|
7
|
+
displayName: "SgNotificationBell",
|
|
8
|
+
category: "feedback",
|
|
9
|
+
subcategory: "notification",
|
|
10
|
+
description: "Sino de notificacoes do usuario logado: contador de nao lidas, lista de ate 100 notificacoes com titulo, texto, data e estado lida/nao lida, marcar como lida, excluir, marcar todas e limpar. Autonomo: conecta-se sozinho ao Pulse (token vindo do backend, sem apikey no front) e reaproveita o SgRealtimeProvider se houver; com o Pulse desligado faz polling de reserva. Sem regra de negocio: cada tipo de notificacao tem icone e acao ao abrir.",
|
|
11
|
+
tags: ["notification", "bell", "inbox", "alert", "realtime", "pulse", "badge", "feedback"],
|
|
12
|
+
capabilities: ["realtime", "polling-fallback", "unread-count", "mark-as-read", "delete", "reduced-motion", "aria-live"],
|
|
13
|
+
fieldSemantics: ["notification", "inbox", "userNotice"],
|
|
14
|
+
props: [
|
|
15
|
+
{ name: "apiUrl", type: "string", default: "/api/user-notifications", description: "Base das rotas da extension user-notifications (BFF, mesma origem).", semanticRole: "data", bindable: true },
|
|
16
|
+
{ name: "tokenEndpoint", type: "string", default: "/api/realtime/connect", description: "Rota que entrega o token do Pulse; usada quando nao ha SgRealtimeProvider acima.", semanticRole: "behavior", bindable: true },
|
|
17
|
+
{ name: "requestInit", type: "RequestInit", description: "Opcoes do fetch (ex.: headers com Authorization quando nao ha cookie).", semanticRole: "behavior", bindable: false },
|
|
18
|
+
{ name: "fetcher", type: "(input: string, init?: RequestInit) => Promise<Response>", description: "Substitui o fetch (apps sem BFF, testes, demonstracao).", semanticRole: "behavior", bindable: false },
|
|
19
|
+
{ name: "types", type: "Record<string, SgNotificationTypeConfig>", description: "Tipo da notificacao -> icone e acao ao abrir (onOpen). O sino nao conhece o negocio.", semanticRole: "data", bindable: true },
|
|
20
|
+
{ name: "eventPattern", type: "string", default: "user-notification.*", description: "Evento do Pulse que dispara a leitura.", semanticRole: "behavior", bindable: true },
|
|
21
|
+
{ name: "pollIntervalMs", type: "number", default: 45000, description: "Intervalo do polling de reserva (so com o Pulse desligado ou fora do ar), preso entre 30 e 60 s, com jitter e backoff em erro.", semanticRole: "behavior", bindable: true },
|
|
22
|
+
{ name: "maxVisible", type: "number", default: 100, description: "Maximo de notificacoes na lista; as mais antigas saem.", semanticRole: "behavior", bindable: true },
|
|
23
|
+
{ name: "placement", type: "\"auto\" | \"top\" | \"right\" | \"bottom\" | \"left\"", default: "auto", description: "De que lado do sino o painel abre (como o placement do SgPopup); auto escolhe o lado em que cabe.", semanticRole: "behavior", bindable: true },
|
|
24
|
+
{ name: "showUnreadToggle", type: "boolean", default: "true", description: "Mostra o interruptor Não lidas no topo do painel; false esconde só o interruptor (o sino fica) e o painel lista tudo.", semanticRole: "behavior", bindable: true },
|
|
25
|
+
{ name: "defaultUnreadOnly", type: "boolean", default: "true", description: "O painel abre em Não lidas (ainda não clicadas); ignorada com showUnreadToggle=false e não persiste entre recargas.", semanticRole: "behavior", bindable: true },
|
|
26
|
+
{ name: "onNotification", type: "(notification: SgNotification) => void", description: "Chegou uma notificacao nova (aviso do Pulse).", semanticRole: "event", bindable: false },
|
|
27
|
+
{ name: "onNotificationDelete", type: "(notification: SgNotification) => void", description: "O usuario excluiu uma notificacao.", semanticRole: "event", bindable: false },
|
|
28
|
+
{ name: "onOpen", type: "(notification: SgNotification) => void", description: "O usuario abriu uma notificacao (depois do onOpen do tipo).", semanticRole: "event", bindable: false },
|
|
29
|
+
{ name: "onPollingTick", type: "(stats: SgPollingStats) => void", description: "O polling consultou a API: contador, instante da ultima consulta e motivo (disabled ou unavailable).", semanticRole: "event", bindable: false }
|
|
30
|
+
],
|
|
31
|
+
states: ["loading", "empty", "list", "error"],
|
|
32
|
+
examples: [
|
|
33
|
+
{
|
|
34
|
+
id: "basic",
|
|
35
|
+
title: "Basico",
|
|
36
|
+
file: "apps/showcase/src/app/components/sg-notification-bell/samples/basico.tsx.sample",
|
|
37
|
+
kind: "sample"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "types",
|
|
41
|
+
title: "Tipos com icone e acao",
|
|
42
|
+
file: "apps/showcase/src/app/components/sg-notification-bell/samples/tipos.tsx.sample",
|
|
43
|
+
kind: "sample"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "playground",
|
|
47
|
+
title: "Playground",
|
|
48
|
+
file: "apps/showcase/src/app/components/sg-notification-bell/sg-notification-bell.tsx.playground",
|
|
49
|
+
kind: "playground"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
showcase: { route: "/components/sg-notification-bell", hasPlayground: true, hasPropsTable: true },
|
|
53
|
+
sdui: { rendererType: "feedback.notification-bell", acceptsDataBinding: true, defaultProps: {} }
|
|
54
|
+
};
|
|
55
|
+
export const aiHints = {
|
|
56
|
+
version: "0.1",
|
|
57
|
+
preferredUseCases: [
|
|
58
|
+
"Avisar o usuario logado de algo que terminou ou aconteceu (relatorio pronto, backup, importacao, convite) com historico e contador de nao lidas.",
|
|
59
|
+
"Barra superior de um app com caixa de notificacoes que atualiza sozinha.",
|
|
60
|
+
"Qualquer aviso persistente ao usuario que nao deve sumir sozinho."
|
|
61
|
+
],
|
|
62
|
+
avoidUseCases: [
|
|
63
|
+
"Mensagem rapida que some sozinha (toast): use o feedback de toast.",
|
|
64
|
+
"Chat ou mensagens entre usuarios.",
|
|
65
|
+
"Mostrar notificacoes sem a extension user-notifications no backend."
|
|
66
|
+
],
|
|
67
|
+
synonyms: ["sino", "notificacoes", "notification bell", "inbox", "avisos do usuario", "caixa de notificacoes", "alerts"],
|
|
68
|
+
relatedEntityFields: ["notification", "alert", "unread", "report", "backup"],
|
|
69
|
+
compositionHints: [
|
|
70
|
+
"Pôr no cabecalho do layout autenticado; sem SgRealtimeProvider acima ele abre a propria conexao.",
|
|
71
|
+
"Use types para mapear cada tipo de notificacao a um icone e a acao de abrir (ex.: baixar o relatorio).",
|
|
72
|
+
"O payload da notificacao so tem identificadores: a acao de abrir busca o arquivo ou registro pela API do dono."
|
|
73
|
+
],
|
|
74
|
+
rankingSignals: { freeText: 0, structuredChoice: 0, date: 0, number: 0.2, denseLayout: 0.2 }
|
|
75
|
+
};
|