@unofficialbox/box-open-elements 0.5.0 → 0.7.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 +27 -73
- package/dist/components/collections/table.d.ts +113 -3
- package/dist/components/collections/table.js +540 -37
- package/dist/components/feedback/due-badge.d.ts +38 -0
- package/dist/components/feedback/due-badge.js +168 -0
- package/dist/components/feedback/due-types.d.ts +32 -0
- package/dist/components/feedback/due-types.js +91 -0
- package/dist/components/feedback/progress-steps.d.ts +51 -5
- package/dist/components/feedback/progress-steps.js +200 -22
- package/dist/components/feedback/stage-path.d.ts +44 -0
- package/dist/components/feedback/stage-path.js +235 -0
- package/dist/components/forms/select.d.ts +21 -0
- package/dist/components/forms/select.js +100 -3
- package/dist/components/forms/text-field.d.ts +21 -0
- package/dist/components/forms/text-field.js +100 -1
- package/dist/components/layout/nav-sidebar.d.ts +14 -0
- package/dist/components/layout/nav-sidebar.js +49 -0
- package/dist/components/layout/split-view.d.ts +14 -0
- package/dist/components/layout/split-view.js +80 -1
- package/dist/components/overlays/command-palette.d.ts +62 -0
- package/dist/components/overlays/command-palette.js +537 -0
- package/dist/components/overlays/command-types.d.ts +90 -0
- package/dist/components/overlays/command-types.js +259 -0
- package/dist/components/overlays/drawer.d.ts +18 -0
- package/dist/components/overlays/drawer.js +135 -9
- package/dist/components/overlays/shortcuts-overlay.d.ts +48 -0
- package/dist/components/overlays/shortcuts-overlay.js +385 -0
- package/dist/components/overlays/tooltip.js +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +2 -0
- package/dist/core/offset-window.d.ts +70 -0
- package/dist/core/offset-window.js +134 -0
- package/dist/core/virtualize.d.ts +71 -0
- package/dist/core/virtualize.js +67 -0
- package/dist/element-maps.d.ts +292 -0
- package/dist/element-maps.js +1 -0
- package/dist/entries/activity-density.d.ts +1 -0
- package/dist/entries/activity-density.js +1 -0
- package/dist/entries/agent-chat.d.ts +1 -0
- package/dist/entries/agent-chat.js +1 -0
- package/dist/entries/audit-log.d.ts +1 -0
- package/dist/entries/audit-log.js +1 -0
- package/dist/entries/command-palette.d.ts +1 -0
- package/dist/entries/command-palette.js +1 -0
- package/dist/entries/compare-view.d.ts +1 -0
- package/dist/entries/compare-view.js +1 -0
- package/dist/entries/diff-viewer.d.ts +1 -0
- package/dist/entries/diff-viewer.js +1 -0
- package/dist/entries/due-badge.d.ts +1 -0
- package/dist/entries/due-badge.js +1 -0
- package/dist/entries/form-wizard.d.ts +1 -0
- package/dist/entries/form-wizard.js +1 -0
- package/dist/entries/lineage-graph.d.ts +1 -0
- package/dist/entries/lineage-graph.js +1 -0
- package/dist/entries/notification-bell.d.ts +1 -0
- package/dist/entries/notification-bell.js +1 -0
- package/dist/entries/notification-inbox.d.ts +1 -0
- package/dist/entries/notification-inbox.js +1 -0
- package/dist/entries/provenance-strip.d.ts +1 -0
- package/dist/entries/provenance-strip.js +1 -0
- package/dist/entries/run-trace.d.ts +1 -0
- package/dist/entries/run-trace.js +1 -0
- package/dist/entries/shortcuts-overlay.d.ts +1 -0
- package/dist/entries/shortcuts-overlay.js +1 -0
- package/dist/entries/signature-ceremony.d.ts +1 -0
- package/dist/entries/signature-ceremony.js +1 -0
- package/dist/entries/stage-path.d.ts +1 -0
- package/dist/entries/stage-path.js +1 -0
- package/dist/entries/timeline.d.ts +1 -0
- package/dist/entries/timeline.js +1 -0
- package/dist/entries/version-graph.d.ts +1 -0
- package/dist/entries/version-graph.js +1 -0
- package/dist/entries/version-list.d.ts +1 -0
- package/dist/entries/version-list.js +1 -0
- package/dist/entries/wizard-summary.d.ts +1 -0
- package/dist/entries/wizard-summary.js +1 -0
- package/dist/entries/work-queue.d.ts +1 -0
- package/dist/entries/work-queue.js +1 -0
- package/dist/entries/workload-board.d.ts +1 -0
- package/dist/entries/workload-board.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/patterns/agent-chat/agent-chat.d.ts +75 -0
- package/dist/patterns/agent-chat/agent-chat.js +820 -0
- package/dist/patterns/agent-chat/controller.d.ts +27 -0
- package/dist/patterns/agent-chat/controller.js +182 -0
- package/dist/patterns/agent-chat/index.d.ts +3 -0
- package/dist/patterns/agent-chat/index.js +6 -0
- package/dist/patterns/agent-chat/types.d.ts +109 -0
- package/dist/patterns/agent-chat/types.js +1 -0
- package/dist/patterns/audit/activity-density.d.ts +68 -0
- package/dist/patterns/audit/activity-density.js +428 -0
- package/dist/patterns/audit/audit-log.d.ts +182 -0
- package/dist/patterns/audit/audit-log.js +1082 -0
- package/dist/patterns/audit/index.d.ts +3 -0
- package/dist/patterns/audit/index.js +7 -0
- package/dist/patterns/audit/shared.d.ts +10 -0
- package/dist/patterns/audit/shared.js +50 -0
- package/dist/patterns/audit/types.d.ts +143 -0
- package/dist/patterns/audit/types.js +327 -0
- package/dist/patterns/audit/window.d.ts +116 -0
- package/dist/patterns/audit/window.js +132 -0
- package/dist/patterns/content-explorer/actions/controller.js +12 -2
- package/dist/patterns/content-explorer/box-transport.js +117 -25
- package/dist/patterns/content-explorer/content-explorer.d.ts +13 -1
- package/dist/patterns/content-explorer/content-explorer.js +36 -3
- package/dist/patterns/content-explorer/contracts.d.ts +24 -1
- package/dist/patterns/content-explorer/contracts.js +68 -11
- package/dist/patterns/content-explorer/controller.d.ts +16 -1
- package/dist/patterns/content-explorer/controller.js +130 -4
- package/dist/patterns/content-explorer/navigation/controller.js +11 -9
- package/dist/patterns/content-explorer/types.d.ts +58 -1
- package/dist/patterns/content-picker/content-picker.d.ts +71 -0
- package/dist/patterns/content-picker/content-picker.js +747 -0
- package/dist/patterns/content-picker/controller.d.ts +38 -0
- package/dist/patterns/content-picker/controller.js +141 -0
- package/dist/patterns/content-picker/index.d.ts +3 -0
- package/dist/patterns/content-picker/index.js +6 -0
- package/dist/patterns/content-picker/types.d.ts +52 -0
- package/dist/patterns/content-picker/types.js +26 -0
- package/dist/patterns/content-sidebar/content-sidebar.d.ts +43 -0
- package/dist/patterns/content-sidebar/content-sidebar.js +293 -0
- package/dist/patterns/content-sidebar/index.d.ts +2 -0
- package/dist/patterns/content-sidebar/index.js +4 -0
- package/dist/patterns/content-sidebar/types.d.ts +18 -0
- package/dist/patterns/content-sidebar/types.js +20 -0
- package/dist/patterns/content-uploader/box-transport.d.ts +14 -0
- package/dist/patterns/content-uploader/box-transport.js +54 -0
- package/dist/patterns/content-uploader/content-uploader.d.ts +62 -0
- package/dist/patterns/content-uploader/content-uploader.js +559 -0
- package/dist/patterns/content-uploader/controller.d.ts +39 -0
- package/dist/patterns/content-uploader/controller.js +227 -0
- package/dist/patterns/content-uploader/index.d.ts +4 -0
- package/dist/patterns/content-uploader/index.js +8 -0
- package/dist/patterns/content-uploader/types.d.ts +120 -0
- package/dist/patterns/content-uploader/types.js +38 -0
- package/dist/patterns/diff/compare-view.d.ts +47 -0
- package/dist/patterns/diff/compare-view.js +276 -0
- package/dist/patterns/diff/diff-viewer.d.ts +42 -0
- package/dist/patterns/diff/diff-viewer.js +434 -0
- package/dist/patterns/diff/engine.d.ts +22 -0
- package/dist/patterns/diff/engine.js +276 -0
- package/dist/patterns/diff/index.d.ts +4 -0
- package/dist/patterns/diff/index.js +8 -0
- package/dist/patterns/diff/types.d.ts +69 -0
- package/dist/patterns/diff/types.js +24 -0
- package/dist/patterns/form-wizard/controller.d.ts +39 -0
- package/dist/patterns/form-wizard/controller.js +157 -0
- package/dist/patterns/form-wizard/form-wizard.d.ts +63 -0
- package/dist/patterns/form-wizard/form-wizard.js +468 -0
- package/dist/patterns/form-wizard/index.d.ts +4 -0
- package/dist/patterns/form-wizard/index.js +8 -0
- package/dist/patterns/form-wizard/types.d.ts +109 -0
- package/dist/patterns/form-wizard/types.js +83 -0
- package/dist/patterns/form-wizard/wizard-summary.d.ts +62 -0
- package/dist/patterns/form-wizard/wizard-summary.js +324 -0
- package/dist/patterns/internal/safe-href.d.ts +17 -0
- package/dist/patterns/internal/safe-href.js +17 -0
- package/dist/patterns/lineage/index.d.ts +3 -0
- package/dist/patterns/lineage/index.js +6 -0
- package/dist/patterns/lineage/lineage-graph.d.ts +30 -0
- package/dist/patterns/lineage/lineage-graph.js +434 -0
- package/dist/patterns/lineage/provenance-strip.d.ts +22 -0
- package/dist/patterns/lineage/provenance-strip.js +194 -0
- package/dist/patterns/lineage/types.d.ts +53 -0
- package/dist/patterns/lineage/types.js +49 -0
- package/dist/patterns/notifications/index.d.ts +3 -0
- package/dist/patterns/notifications/index.js +6 -0
- package/dist/patterns/notifications/notification-bell.d.ts +38 -0
- package/dist/patterns/notifications/notification-bell.js +198 -0
- package/dist/patterns/notifications/notification-inbox.d.ts +46 -0
- package/dist/patterns/notifications/notification-inbox.js +537 -0
- package/dist/patterns/notifications/types.d.ts +61 -0
- package/dist/patterns/notifications/types.js +108 -0
- package/dist/patterns/preview/annotation-inspector.d.ts +25 -6
- package/dist/patterns/preview/annotation-inspector.js +239 -94
- package/dist/patterns/preview/annotation-thread.d.ts +25 -5
- package/dist/patterns/preview/annotation-thread.js +233 -82
- package/dist/patterns/preview/annotation-toolbar.d.ts +20 -6
- package/dist/patterns/preview/annotation-toolbar.js +162 -96
- package/dist/patterns/preview/content-preview-adapter.d.ts +10 -1
- package/dist/patterns/preview/content-preview-adapter.js +26 -1
- package/dist/patterns/preview/preview-element.d.ts +15 -2
- package/dist/patterns/preview/preview-element.js +223 -52
- package/dist/patterns/preview/provider-adapter.d.ts +47 -0
- package/dist/patterns/preview/provider-adapter.js +17 -0
- package/dist/patterns/run/index.d.ts +2 -0
- package/dist/patterns/run/index.js +4 -0
- package/dist/patterns/run/run-trace.d.ts +40 -0
- package/dist/patterns/run/run-trace.js +441 -0
- package/dist/patterns/run/types.d.ts +66 -0
- package/dist/patterns/run/types.js +152 -0
- package/dist/patterns/signature/index.d.ts +2 -0
- package/dist/patterns/signature/index.js +4 -0
- package/dist/patterns/signature/signature-ceremony.d.ts +33 -0
- package/dist/patterns/signature/signature-ceremony.js +317 -0
- package/dist/patterns/signature/types.d.ts +68 -0
- package/dist/patterns/signature/types.js +89 -0
- package/dist/patterns/timeline/index.d.ts +2 -0
- package/dist/patterns/timeline/index.js +4 -0
- package/dist/patterns/timeline/timeline.d.ts +27 -0
- package/dist/patterns/timeline/timeline.js +430 -0
- package/dist/patterns/timeline/types.d.ts +36 -0
- package/dist/patterns/timeline/types.js +27 -0
- package/dist/patterns/versions/graph-layout.d.ts +43 -0
- package/dist/patterns/versions/graph-layout.js +159 -0
- package/dist/patterns/versions/index.d.ts +4 -0
- package/dist/patterns/versions/index.js +8 -0
- package/dist/patterns/versions/shared.d.ts +4 -0
- package/dist/patterns/versions/shared.js +13 -0
- package/dist/patterns/versions/types.d.ts +37 -0
- package/dist/patterns/versions/types.js +39 -0
- package/dist/patterns/versions/version-graph.d.ts +36 -0
- package/dist/patterns/versions/version-graph.js +438 -0
- package/dist/patterns/versions/version-list.d.ts +39 -0
- package/dist/patterns/versions/version-list.js +407 -0
- package/dist/patterns/work-queue/controller.d.ts +26 -0
- package/dist/patterns/work-queue/controller.js +150 -0
- package/dist/patterns/work-queue/index.d.ts +4 -0
- package/dist/patterns/work-queue/index.js +7 -0
- package/dist/patterns/work-queue/types.d.ts +128 -0
- package/dist/patterns/work-queue/types.js +45 -0
- package/dist/patterns/work-queue/work-queue.d.ts +48 -0
- package/dist/patterns/work-queue/work-queue.js +534 -0
- package/dist/patterns/work-queue/workload-board.d.ts +53 -0
- package/dist/patterns/work-queue/workload-board.js +608 -0
- package/package.json +89 -5
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { countUnreadNotifications, groupNotifications, isNotificationItemRecord, resolveNotificationFilter, resolveNotificationTone, } from "./types.js";
|
|
2
|
+
import { BaseElement } from "../../core/index.js";
|
|
3
|
+
import { isSafeHref } from "../internal/safe-href.js";
|
|
4
|
+
import { boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
5
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
6
|
+
const DEFAULT_TAG_NAME = "box-notification-inbox";
|
|
7
|
+
const escapeHtml = (value) => value
|
|
8
|
+
.replaceAll("&", "&")
|
|
9
|
+
.replaceAll("<", "<")
|
|
10
|
+
.replaceAll(">", ">")
|
|
11
|
+
.replaceAll('"', """)
|
|
12
|
+
.replaceAll("'", "'");
|
|
13
|
+
const toneColor = (tone) => {
|
|
14
|
+
switch (tone) {
|
|
15
|
+
case "brand":
|
|
16
|
+
return "var(--boe-token-surface-surface-brand, #0061d5)";
|
|
17
|
+
case "success":
|
|
18
|
+
return "var(--boe-token-surface-status-surface-success, #26a27b)";
|
|
19
|
+
case "warning":
|
|
20
|
+
return "var(--boe-token-surface-status-surface-warning, #f5b31b)";
|
|
21
|
+
case "error":
|
|
22
|
+
return "var(--boe-token-surface-status-surface-error, #ed3757)";
|
|
23
|
+
default:
|
|
24
|
+
return "var(--boe-token-text-text-secondary, #6f6f6f)";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const elementStyles = `
|
|
28
|
+
[hidden] {
|
|
29
|
+
display: none !important;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:host {
|
|
33
|
+
display: block;
|
|
34
|
+
color: inherit;
|
|
35
|
+
font: inherit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[part="panel"] {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-rows: auto auto 1fr;
|
|
41
|
+
gap: ${boePanel.gap};
|
|
42
|
+
padding: ${boePanel.padding};
|
|
43
|
+
max-block-size: 32rem;
|
|
44
|
+
border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
|
|
45
|
+
border-radius: ${boePanel.radius};
|
|
46
|
+
background: ${boePanel.background};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[part="header"] {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 0.5rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[part="title"] {
|
|
56
|
+
margin: 0;
|
|
57
|
+
font: inherit;
|
|
58
|
+
font-size: 1.05rem;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[part="unread-pill"] {
|
|
64
|
+
padding: 0.1rem 0.45rem;
|
|
65
|
+
border-radius: 999px;
|
|
66
|
+
background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 14%, transparent);
|
|
67
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 78%, black 22%);
|
|
68
|
+
font-size: 0.72rem;
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[part="mark-all"] {
|
|
73
|
+
margin-inline-start: auto;
|
|
74
|
+
appearance: none;
|
|
75
|
+
font: inherit;
|
|
76
|
+
font-size: 0.78rem;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
padding: 0.3rem 0.55rem;
|
|
79
|
+
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
80
|
+
border-radius: ${boeRadius.control};
|
|
81
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
82
|
+
color: var(--boe-token-text-text, #222222);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[part="mark-all"]:hover:not(:disabled) {
|
|
87
|
+
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[part="mark-all"]:disabled {
|
|
91
|
+
opacity: 0.45;
|
|
92
|
+
cursor: not-allowed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
[part="filters"] {
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
98
|
+
border-radius: 999px;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
justify-self: start;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[part="filter"] {
|
|
104
|
+
appearance: none;
|
|
105
|
+
font: inherit;
|
|
106
|
+
font-size: 0.78rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
padding: 0.25rem 0.7rem;
|
|
109
|
+
border: none;
|
|
110
|
+
background: transparent;
|
|
111
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[part="filter"][aria-pressed="true"] {
|
|
116
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 12%, transparent);
|
|
117
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
[part="groups"] {
|
|
121
|
+
margin: 0;
|
|
122
|
+
padding: 0;
|
|
123
|
+
list-style: none;
|
|
124
|
+
display: grid;
|
|
125
|
+
gap: 0.6rem;
|
|
126
|
+
overflow-y: auto;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[part="group-label"] {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: baseline;
|
|
132
|
+
gap: 0.4rem;
|
|
133
|
+
padding-block-end: 0.2rem;
|
|
134
|
+
font-size: 0.7rem;
|
|
135
|
+
font-weight: 700;
|
|
136
|
+
letter-spacing: 0.08em;
|
|
137
|
+
text-transform: uppercase;
|
|
138
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[part="group-unread"] {
|
|
142
|
+
text-transform: none;
|
|
143
|
+
letter-spacing: normal;
|
|
144
|
+
font-size: 0.7rem;
|
|
145
|
+
color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 78%, black 22%);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
[part="items"] {
|
|
149
|
+
margin: 0;
|
|
150
|
+
padding: 0;
|
|
151
|
+
list-style: none;
|
|
152
|
+
display: grid;
|
|
153
|
+
gap: 0.3rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
[part="item"] {
|
|
157
|
+
display: grid;
|
|
158
|
+
grid-template-columns: auto 1fr auto;
|
|
159
|
+
gap: 0.5rem;
|
|
160
|
+
align-items: start;
|
|
161
|
+
padding: 0.5rem 0.55rem;
|
|
162
|
+
border-radius: ${boeRadius.med};
|
|
163
|
+
border-left: 3px solid var(--notification-tone, transparent);
|
|
164
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 6%, transparent);
|
|
165
|
+
transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
[part="item"][data-read="true"] {
|
|
169
|
+
background: transparent;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[part="unread-dot"] {
|
|
173
|
+
inline-size: 0.5rem;
|
|
174
|
+
block-size: 0.5rem;
|
|
175
|
+
margin-block-start: 0.4rem;
|
|
176
|
+
border-radius: 999px;
|
|
177
|
+
background: var(--boe-token-surface-surface-brand, #0061d5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[part="item"][data-read="true"] [part="unread-dot"] {
|
|
181
|
+
background: transparent;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
[part="item-body"] {
|
|
185
|
+
display: grid;
|
|
186
|
+
gap: 0.15rem;
|
|
187
|
+
min-inline-size: 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
[part="item-title"] {
|
|
191
|
+
appearance: none;
|
|
192
|
+
padding: 0;
|
|
193
|
+
border: none;
|
|
194
|
+
background: none;
|
|
195
|
+
font: inherit;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
text-align: start;
|
|
198
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
[part="item"][data-read="true"] [part="item-title"] {
|
|
203
|
+
font-weight: 400;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
[part="item-title"]:hover {
|
|
207
|
+
text-decoration: underline;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
[part="item-meta"] {
|
|
211
|
+
display: flex;
|
|
212
|
+
flex-wrap: wrap;
|
|
213
|
+
gap: 0.35rem;
|
|
214
|
+
font-size: 0.76rem;
|
|
215
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
[part="summary"] {
|
|
219
|
+
margin: 0;
|
|
220
|
+
font-size: 0.82rem;
|
|
221
|
+
line-height: 1.45;
|
|
222
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
[part="entity"] {
|
|
226
|
+
align-self: start;
|
|
227
|
+
font-size: 0.76rem;
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
color: var(--boe-token-surface-surface-brand, #0061d5);
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
[part="item-actions"] {
|
|
234
|
+
display: flex;
|
|
235
|
+
gap: 0.25rem;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
[part="item-action"] {
|
|
239
|
+
appearance: none;
|
|
240
|
+
font: inherit;
|
|
241
|
+
font-size: 0.72rem;
|
|
242
|
+
padding: 0.2rem 0.4rem;
|
|
243
|
+
border: 1px solid transparent;
|
|
244
|
+
border-radius: ${boeRadius.size};
|
|
245
|
+
background: none;
|
|
246
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
[part="item-action"]:hover {
|
|
251
|
+
border-color: var(--boe-token-stroke-stroke, #e8e8e8);
|
|
252
|
+
color: var(--boe-token-text-text, #1f1e1b);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
[part="empty"] {
|
|
256
|
+
padding: ${boePanel.padding};
|
|
257
|
+
border-radius: ${boeRadius.large};
|
|
258
|
+
border: 1px dashed color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 70%, transparent);
|
|
259
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
260
|
+
text-align: center;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
[part="filter"]:focus-visible,
|
|
264
|
+
[part="mark-all"]:focus-visible,
|
|
265
|
+
[part="item-title"]:focus-visible,
|
|
266
|
+
[part="item-action"]:focus-visible,
|
|
267
|
+
[part="entity"]:focus-visible {
|
|
268
|
+
outline: none;
|
|
269
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@media (prefers-reduced-motion: reduce) {
|
|
273
|
+
[part="item"] {
|
|
274
|
+
transition: none;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
`;
|
|
278
|
+
/**
|
|
279
|
+
* The triage inbox: approvals waiting, SLA breaches, mentions — grouped by
|
|
280
|
+
* type, filterable to unread, with per-row and bulk intents.
|
|
281
|
+
*
|
|
282
|
+
* Toasts are the wrong surface for this: they are transient and unordered,
|
|
283
|
+
* while triage needs a list you can come back to. Sections lead with the most
|
|
284
|
+
* unread so what needs attention rises to the top.
|
|
285
|
+
*
|
|
286
|
+
* Mutations are intents, not state changes. The element never marks anything
|
|
287
|
+
* read on its own — the host owns the write and feeds back a new list, so the
|
|
288
|
+
* inbox can never disagree with the server about what has been seen.
|
|
289
|
+
*/
|
|
290
|
+
export class NotificationInbox extends BaseElement {
|
|
291
|
+
static tagName = DEFAULT_TAG_NAME;
|
|
292
|
+
static get observedAttributes() {
|
|
293
|
+
return ["filter", "heading", "notifications", "type-labels"];
|
|
294
|
+
}
|
|
295
|
+
panelEl;
|
|
296
|
+
titleEl;
|
|
297
|
+
unreadPillEl;
|
|
298
|
+
markAllEl;
|
|
299
|
+
filtersEl;
|
|
300
|
+
groupsEl;
|
|
301
|
+
emptyEl;
|
|
302
|
+
signature = "";
|
|
303
|
+
notificationsRaw = null;
|
|
304
|
+
notificationsCache = [];
|
|
305
|
+
get heading() {
|
|
306
|
+
return this.getAttribute("heading") ?? "Notifications";
|
|
307
|
+
}
|
|
308
|
+
set heading(value) {
|
|
309
|
+
this.setAttribute("heading", value);
|
|
310
|
+
}
|
|
311
|
+
get filter() {
|
|
312
|
+
return resolveNotificationFilter(this.getAttribute("filter"));
|
|
313
|
+
}
|
|
314
|
+
set filter(value) {
|
|
315
|
+
this.setAttribute("filter", resolveNotificationFilter(value));
|
|
316
|
+
}
|
|
317
|
+
/** Notification records; JSON payloads are validated per record. */
|
|
318
|
+
get notifications() {
|
|
319
|
+
const raw = this.getAttribute("notifications");
|
|
320
|
+
if (!raw) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
if (raw !== this.notificationsRaw) {
|
|
324
|
+
this.notificationsRaw = raw;
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(raw);
|
|
327
|
+
this.notificationsCache =
|
|
328
|
+
Array.isArray(parsed) && parsed.every(isNotificationItemRecord)
|
|
329
|
+
? parsed
|
|
330
|
+
: [];
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
this.notificationsCache = [];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return [...this.notificationsCache];
|
|
337
|
+
}
|
|
338
|
+
set notifications(value) {
|
|
339
|
+
if (value.length) {
|
|
340
|
+
this.setAttribute("notifications", JSON.stringify(value));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
this.removeAttribute("notifications");
|
|
344
|
+
}
|
|
345
|
+
/** Optional `{ type: heading }` map; unmapped types are humanized. */
|
|
346
|
+
get typeLabels() {
|
|
347
|
+
const raw = this.getAttribute("type-labels");
|
|
348
|
+
if (!raw) {
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const parsed = JSON.parse(raw);
|
|
353
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
const entries = Object.entries(parsed).filter(([, value]) => typeof value === "string");
|
|
357
|
+
return Object.fromEntries(entries);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
set typeLabels(value) {
|
|
364
|
+
if (Object.keys(value).length) {
|
|
365
|
+
this.setAttribute("type-labels", JSON.stringify(value));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.removeAttribute("type-labels");
|
|
369
|
+
}
|
|
370
|
+
get unreadCount() {
|
|
371
|
+
return countUnreadNotifications(this.notifications);
|
|
372
|
+
}
|
|
373
|
+
/** The records currently on screen, after the filter. */
|
|
374
|
+
get visibleNotifications() {
|
|
375
|
+
return groupNotifications(this.notifications, {
|
|
376
|
+
filter: this.filter,
|
|
377
|
+
typeLabels: this.typeLabels,
|
|
378
|
+
}).flatMap(group => group.items);
|
|
379
|
+
}
|
|
380
|
+
emitIntent(name, detail) {
|
|
381
|
+
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|
|
382
|
+
}
|
|
383
|
+
itemHtml(item) {
|
|
384
|
+
const tone = resolveNotificationTone(item.tone);
|
|
385
|
+
const read = Boolean(item.read);
|
|
386
|
+
const entity = item.entityRef;
|
|
387
|
+
const entityHtml = entity
|
|
388
|
+
? entity.href && isSafeHref(entity.href)
|
|
389
|
+
? `<a part="entity" href="${escapeHtml(entity.href)}" data-item-id="${escapeHtml(item.id)}">${escapeHtml(entity.label)}</a>`
|
|
390
|
+
: `<span part="entity">${escapeHtml(entity.label)}</span>`
|
|
391
|
+
: "";
|
|
392
|
+
return `
|
|
393
|
+
<li part="item" data-item-id="${escapeHtml(item.id)}" data-read="${String(read)}" data-type="${escapeHtml(item.type)}" style="--notification-tone:${toneColor(tone)};">
|
|
394
|
+
<span part="unread-dot" aria-hidden="true"></span>
|
|
395
|
+
<span part="item-body">
|
|
396
|
+
<button type="button" part="item-title" data-item-id="${escapeHtml(item.id)}">${escapeHtml(item.title)}</button>
|
|
397
|
+
${item.summary ? `<p part="summary">${escapeHtml(item.summary)}</p>` : ""}
|
|
398
|
+
<span part="item-meta">
|
|
399
|
+
${item.actor ? `<span part="actor">${escapeHtml(item.actor.name)}</span>` : ""}
|
|
400
|
+
${item.timestamp ? `<time part="timestamp" datetime="${escapeHtml(item.timestamp)}">${escapeHtml(item.timestamp)}</time>` : ""}
|
|
401
|
+
${entityHtml}
|
|
402
|
+
</span>
|
|
403
|
+
</span>
|
|
404
|
+
<span part="item-actions">
|
|
405
|
+
${read
|
|
406
|
+
? ""
|
|
407
|
+
: `<button type="button" part="item-action" data-action="read" data-item-id="${escapeHtml(item.id)}" aria-label="Mark ${escapeHtml(item.title)} as read">Mark read</button>`}
|
|
408
|
+
<button type="button" part="item-action" data-action="dismiss" data-item-id="${escapeHtml(item.id)}" aria-label="Dismiss ${escapeHtml(item.title)}">Dismiss</button>
|
|
409
|
+
</span>
|
|
410
|
+
</li>
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
renderTemplate() {
|
|
414
|
+
if (!this.shadowRoot) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.shadowRoot.innerHTML = `
|
|
418
|
+
<style>${elementStyles}</style>
|
|
419
|
+
<section part="panel" aria-labelledby="inbox-title">
|
|
420
|
+
<div part="header">
|
|
421
|
+
<h2 part="title" id="inbox-title"></h2>
|
|
422
|
+
<span part="unread-pill" hidden></span>
|
|
423
|
+
<button type="button" part="mark-all">Mark all read</button>
|
|
424
|
+
</div>
|
|
425
|
+
<div part="filters" role="group" aria-label="Filter notifications">
|
|
426
|
+
<button type="button" part="filter" data-filter="all" aria-pressed="true">All</button>
|
|
427
|
+
<button type="button" part="filter" data-filter="unread" aria-pressed="false">Unread</button>
|
|
428
|
+
</div>
|
|
429
|
+
<ul part="groups" role="list"></ul>
|
|
430
|
+
<div part="empty" hidden>You're all caught up.</div>
|
|
431
|
+
</section>
|
|
432
|
+
`;
|
|
433
|
+
const root = this.shadowRoot;
|
|
434
|
+
this.panelEl = root.querySelector('[part="panel"]');
|
|
435
|
+
this.titleEl = root.querySelector('[part="title"]');
|
|
436
|
+
this.unreadPillEl = root.querySelector('[part="unread-pill"]');
|
|
437
|
+
this.markAllEl = root.querySelector('[part="mark-all"]');
|
|
438
|
+
this.filtersEl = root.querySelector('[part="filters"]');
|
|
439
|
+
this.groupsEl = root.querySelector('[part="groups"]');
|
|
440
|
+
this.emptyEl = root.querySelector('[part="empty"]');
|
|
441
|
+
}
|
|
442
|
+
setupListeners() {
|
|
443
|
+
this.filtersEl.addEventListener("click", event => {
|
|
444
|
+
const button = event.target.closest('[part="filter"]');
|
|
445
|
+
if (!button) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const next = resolveNotificationFilter(button.getAttribute("data-filter"));
|
|
449
|
+
if (next === this.filter) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.filter = next;
|
|
453
|
+
this.emitIntent("filter-changed", { filter: next });
|
|
454
|
+
});
|
|
455
|
+
this.markAllEl.addEventListener("click", () => {
|
|
456
|
+
const unread = this.notifications.filter(item => !item.read);
|
|
457
|
+
if (unread.length === 0) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.emitIntent("mark-all-read-requested", { items: unread });
|
|
461
|
+
});
|
|
462
|
+
this.groupsEl.addEventListener("click", event => {
|
|
463
|
+
const target = event.target;
|
|
464
|
+
const action = target.closest('[part="item-action"]');
|
|
465
|
+
if (action && this.groupsEl.contains(action)) {
|
|
466
|
+
const item = this.notifications.find(entry => entry.id === (action.getAttribute("data-item-id") ?? ""));
|
|
467
|
+
if (!item) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.emitIntent(action.getAttribute("data-action") === "read"
|
|
471
|
+
? "mark-read-requested"
|
|
472
|
+
: "dismiss-requested", { item });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const title = target.closest('[part="item-title"]');
|
|
476
|
+
if (title && this.groupsEl.contains(title)) {
|
|
477
|
+
const item = this.notifications.find(entry => entry.id === (title.getAttribute("data-item-id") ?? ""));
|
|
478
|
+
if (item) {
|
|
479
|
+
this.emitIntent("notification-selected", { item });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
update() {
|
|
485
|
+
if (!this.groupsEl) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const all = this.notifications;
|
|
489
|
+
const unread = countUnreadNotifications(all);
|
|
490
|
+
const filter = this.filter;
|
|
491
|
+
this.titleEl.textContent = this.heading;
|
|
492
|
+
this.unreadPillEl.hidden = unread === 0;
|
|
493
|
+
this.unreadPillEl.textContent = `${String(unread)} unread`;
|
|
494
|
+
this.markAllEl.disabled = unread === 0;
|
|
495
|
+
for (const button of Array.from(this.filtersEl.querySelectorAll('[part="filter"]'))) {
|
|
496
|
+
button.setAttribute("aria-pressed", button.getAttribute("data-filter") === filter ? "true" : "false");
|
|
497
|
+
}
|
|
498
|
+
const groups = groupNotifications(all, { filter, typeLabels: this.typeLabels });
|
|
499
|
+
const signature = JSON.stringify([this.getAttribute("notifications") ?? "", filter, this.typeLabels]);
|
|
500
|
+
if (signature !== this.signature) {
|
|
501
|
+
this.signature = signature;
|
|
502
|
+
// Sample focus before the rebuild so acting on a row does not drop the
|
|
503
|
+
// reader back to the top of the panel.
|
|
504
|
+
const active = this.shadowRoot?.activeElement;
|
|
505
|
+
const focusKey = active && this.groupsEl.contains(active)
|
|
506
|
+
? {
|
|
507
|
+
part: active.getAttribute("part") ?? "",
|
|
508
|
+
itemId: active.getAttribute("data-item-id"),
|
|
509
|
+
action: active.getAttribute("data-action"),
|
|
510
|
+
}
|
|
511
|
+
: null;
|
|
512
|
+
this.groupsEl.innerHTML = groups
|
|
513
|
+
.map(group => `
|
|
514
|
+
<li part="group" data-group-key="${escapeHtml(group.key)}">
|
|
515
|
+
<span part="group-label">
|
|
516
|
+
${escapeHtml(group.label)}
|
|
517
|
+
${group.unread ? `<span part="group-unread">${String(group.unread)} unread</span>` : ""}
|
|
518
|
+
</span>
|
|
519
|
+
<ul part="items" role="list">${group.items.map(item => this.itemHtml(item)).join("")}</ul>
|
|
520
|
+
</li>
|
|
521
|
+
`)
|
|
522
|
+
.join("");
|
|
523
|
+
if (focusKey?.part) {
|
|
524
|
+
const restored = Array.from(this.groupsEl.querySelectorAll(`[part="${focusKey.part}"]`)).find(node => node.getAttribute("data-item-id") === focusKey.itemId &&
|
|
525
|
+
node.getAttribute("data-action") === focusKey.action);
|
|
526
|
+
restored?.focus();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const empty = groups.length === 0;
|
|
530
|
+
this.groupsEl.hidden = empty;
|
|
531
|
+
this.emptyEl.hidden = !empty;
|
|
532
|
+
this.emptyEl.textContent =
|
|
533
|
+
filter === "unread" && all.length > 0 ? "No unread notifications." : "You're all caught up.";
|
|
534
|
+
this.panelEl.setAttribute("aria-label", this.heading);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
NotificationInbox.register();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification model and the pure grouping/counting engine behind
|
|
3
|
+
* `box-notification-inbox`. DOM-free, so a host can drive its own inbox —
|
|
4
|
+
* or a bell in a different shell — from the same functions.
|
|
5
|
+
*/
|
|
6
|
+
export type NotificationTone = "neutral" | "brand" | "success" | "warning" | "error";
|
|
7
|
+
/** The entity a notification points at; the row links to it when `href` is safe. */
|
|
8
|
+
export interface NotificationEntityRef {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One triage-worthy event: an approval waiting, an SLA breach, a mention.
|
|
15
|
+
* `type` is an open vocabulary — hosts add their own kinds without a schema
|
|
16
|
+
* change, and the inbox groups by whatever it finds.
|
|
17
|
+
*/
|
|
18
|
+
export interface NotificationItem {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Short imperative headline: "Approval needed on MSA_Acme_v4". */
|
|
21
|
+
title: string;
|
|
22
|
+
type: string;
|
|
23
|
+
read?: boolean;
|
|
24
|
+
actor?: {
|
|
25
|
+
name: string;
|
|
26
|
+
initials?: string;
|
|
27
|
+
};
|
|
28
|
+
summary?: string;
|
|
29
|
+
/** ISO timestamp; ordering is by this, newest first. */
|
|
30
|
+
timestamp?: string;
|
|
31
|
+
tone?: NotificationTone;
|
|
32
|
+
entityRef?: NotificationEntityRef;
|
|
33
|
+
}
|
|
34
|
+
export interface NotificationGroup {
|
|
35
|
+
/** The `type` value; also the collapse/section key. */
|
|
36
|
+
key: string;
|
|
37
|
+
label: string;
|
|
38
|
+
items: NotificationItem[];
|
|
39
|
+
unread: number;
|
|
40
|
+
}
|
|
41
|
+
export type NotificationFilter = "all" | "unread";
|
|
42
|
+
export declare const resolveNotificationTone: (value: string | null | undefined) => NotificationTone;
|
|
43
|
+
export declare const resolveNotificationFilter: (value: string | null | undefined) => NotificationFilter;
|
|
44
|
+
/** Unread count — what the bell shows, and the only number that must be exact. */
|
|
45
|
+
export declare const countUnreadNotifications: (items: readonly NotificationItem[]) => number;
|
|
46
|
+
export interface GroupNotificationsOptions {
|
|
47
|
+
filter?: NotificationFilter;
|
|
48
|
+
/** Override the derived heading for a `type`. */
|
|
49
|
+
typeLabels?: Readonly<Record<string, string>>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Group notifications into sections by type.
|
|
53
|
+
*
|
|
54
|
+
* Sections lead with the most unread, then the most items, then the label —
|
|
55
|
+
* so what needs attention rises, and the order never depends on input order.
|
|
56
|
+
* Items inside a section run newest first, with unread ahead of read at the
|
|
57
|
+
* same instant: this is a triage surface, not a feed.
|
|
58
|
+
*/
|
|
59
|
+
export declare const groupNotifications: (items: readonly NotificationItem[], options?: GroupNotificationsOptions) => NotificationGroup[];
|
|
60
|
+
/** Attribute payloads are author input — validate every record. */
|
|
61
|
+
export declare const isNotificationItemRecord: (value: unknown) => value is NotificationItem;
|