@rapidmx/web-client 0.2.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.
Files changed (168) hide show
  1. package/LICENSE +382 -0
  2. package/apps/admin/_404.tsx +9 -0
  3. package/apps/admin/_500.tsx +9 -0
  4. package/apps/admin/_layout.tsx +41 -0
  5. package/apps/admin/audit-log/index.tsx +187 -0
  6. package/apps/admin/branding/index.tsx +404 -0
  7. package/apps/admin/distribution-lists/[uid].tsx +70 -0
  8. package/apps/admin/distribution-lists/index.tsx +115 -0
  9. package/apps/admin/distribution-lists/new/index.tsx +114 -0
  10. package/apps/admin/domains/[uid].tsx +259 -0
  11. package/apps/admin/domains/index.tsx +125 -0
  12. package/apps/admin/domains/new/index.tsx +84 -0
  13. package/apps/admin/index.tsx +82 -0
  14. package/apps/admin/ingest-queue/index.tsx +116 -0
  15. package/apps/admin/mailboxes/[uid].tsx +126 -0
  16. package/apps/admin/mailboxes/new/index.tsx +291 -0
  17. package/apps/admin/quarantine/index.tsx +171 -0
  18. package/apps/admin/transport-rules/[uid].tsx +143 -0
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
  20. package/apps/admin/transport-rules/index.tsx +117 -0
  21. package/apps/admin/transport-rules/new/index.tsx +105 -0
  22. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
  23. package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
  24. package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
  25. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
  26. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
  27. package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
  29. package/apps/shared/components/calendar/EventModal.tsx +513 -0
  30. package/apps/shared/components/calendar/MonthView.tsx +139 -0
  31. package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
  32. package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
  33. package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
  34. package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
  35. package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
  36. package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
  37. package/apps/shared/components/contacts/ContactForm.tsx +314 -0
  38. package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
  39. package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
  40. package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
  41. package/apps/shared/components/layout/AppShell.tsx +165 -0
  42. package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
  43. package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
  44. package/apps/shared/components/layout/UserMenu.tsx +150 -0
  45. package/apps/shared/components/mail/ConversationList.tsx +61 -0
  46. package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
  49. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
  50. package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
  51. package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
  52. package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
  53. package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
  54. package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
  55. package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
  56. package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
  57. package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
  58. package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
  59. package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
  60. package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
  61. package/apps/shared/styles/app.css +265 -0
  62. package/apps/www/_404.tsx +9 -0
  63. package/apps/www/_500.tsx +9 -0
  64. package/apps/www/_layout.tsx +45 -0
  65. package/apps/www/calendar/index.tsx +407 -0
  66. package/apps/www/contacts/[uid].tsx +88 -0
  67. package/apps/www/contacts/index.tsx +436 -0
  68. package/apps/www/index.tsx +375 -0
  69. package/apps/www/messages/[uid].tsx +70 -0
  70. package/apps/www/settings/auto-reply/index.tsx +129 -0
  71. package/apps/www/settings/booking-types/[uid].tsx +300 -0
  72. package/apps/www/settings/booking-types/index.tsx +98 -0
  73. package/apps/www/settings/booking-types/new/index.tsx +212 -0
  74. package/apps/www/settings/filters/[uid].tsx +155 -0
  75. package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
  76. package/apps/www/settings/filters/index.tsx +99 -0
  77. package/apps/www/settings/filters/new/index.tsx +126 -0
  78. package/apps/www/settings/focused-inbox/index.tsx +148 -0
  79. package/apps/www/settings/read-receipts/index.tsx +125 -0
  80. package/apps/www/settings/signatures/[uid].tsx +167 -0
  81. package/apps/www/settings/signatures/index.tsx +85 -0
  82. package/apps/www/settings/signatures/new/index.tsx +129 -0
  83. package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
  84. package/apps/www/tasks/index.tsx +530 -0
  85. package/dist/apps/admin/_404.js +4 -0
  86. package/dist/apps/admin/_500.js +4 -0
  87. package/dist/apps/admin/_layout.js +10 -0
  88. package/dist/apps/admin/audit-log/index.js +66 -0
  89. package/dist/apps/admin/branding/index.js +126 -0
  90. package/dist/apps/admin/distribution-lists/[uid].js +34 -0
  91. package/dist/apps/admin/distribution-lists/index.js +31 -0
  92. package/dist/apps/admin/distribution-lists/new/index.js +51 -0
  93. package/dist/apps/admin/domains/[uid].js +102 -0
  94. package/dist/apps/admin/domains/index.js +31 -0
  95. package/dist/apps/admin/domains/new/index.js +41 -0
  96. package/dist/apps/admin/index.js +32 -0
  97. package/dist/apps/admin/ingest-queue/index.js +48 -0
  98. package/dist/apps/admin/mailboxes/[uid].js +59 -0
  99. package/dist/apps/admin/mailboxes/new/index.js +93 -0
  100. package/dist/apps/admin/quarantine/index.js +66 -0
  101. package/dist/apps/admin/transport-rules/[uid].js +87 -0
  102. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
  103. package/dist/apps/admin/transport-rules/index.js +32 -0
  104. package/dist/apps/admin/transport-rules/new/index.js +50 -0
  105. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
  106. package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
  107. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
  108. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
  109. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
  110. package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
  111. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
  112. package/dist/apps/shared/components/calendar/EventModal.js +167 -0
  113. package/dist/apps/shared/components/calendar/MonthView.js +38 -0
  114. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
  115. package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
  116. package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
  117. package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
  118. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
  119. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
  120. package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
  121. package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
  122. package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
  123. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
  124. package/dist/apps/shared/components/layout/AppShell.js +68 -0
  125. package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
  126. package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
  127. package/dist/apps/shared/components/layout/UserMenu.js +59 -0
  128. package/dist/apps/shared/components/mail/ConversationList.js +17 -0
  129. package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
  130. package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
  131. package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
  132. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
  133. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
  134. package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
  135. package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
  136. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
  137. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
  138. package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
  139. package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
  140. package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
  141. package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
  142. package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
  143. package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
  144. package/dist/apps/shared/styles/app.css +265 -0
  145. package/dist/apps/www/_404.js +4 -0
  146. package/dist/apps/www/_500.js +4 -0
  147. package/dist/apps/www/_layout.js +8 -0
  148. package/dist/apps/www/calendar/index.js +228 -0
  149. package/dist/apps/www/contacts/[uid].js +62 -0
  150. package/dist/apps/www/contacts/index.js +262 -0
  151. package/dist/apps/www/index.js +228 -0
  152. package/dist/apps/www/messages/[uid].js +47 -0
  153. package/dist/apps/www/settings/auto-reply/index.js +54 -0
  154. package/dist/apps/www/settings/booking-types/[uid].js +129 -0
  155. package/dist/apps/www/settings/booking-types/index.js +29 -0
  156. package/dist/apps/www/settings/booking-types/new/index.js +86 -0
  157. package/dist/apps/www/settings/filters/[uid].js +94 -0
  158. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
  159. package/dist/apps/www/settings/filters/index.js +30 -0
  160. package/dist/apps/www/settings/filters/new/index.js +61 -0
  161. package/dist/apps/www/settings/focused-inbox/index.js +69 -0
  162. package/dist/apps/www/settings/read-receipts/index.js +51 -0
  163. package/dist/apps/www/settings/signatures/[uid].js +96 -0
  164. package/dist/apps/www/settings/signatures/index.js +34 -0
  165. package/dist/apps/www/settings/signatures/new/index.js +63 -0
  166. package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
  167. package/dist/apps/www/tasks/index.js +280 -0
  168. package/package.json +87 -0
@@ -0,0 +1,265 @@
1
+ /*
2
+ * RapidMX mail-server UI — Tailwind entry point.
3
+ *
4
+ * Design tokens are still defined as plain CSS custom properties (ported from auth-server's
5
+ * `public/styles/globals.css`, itself from rapidrest.dev's Infima-based cyan/gold palette) rather than
6
+ * inside the `@theme` block directly, so the existing `prefers-color-scheme`/`[data-theme]` dark-mode
7
+ * mechanism keeps working exactly as before. The `@theme` block below just aliases Tailwind's own color/
8
+ * radius/shadow/font tokens to these variables, so `bg-primary`, `text-muted`, `rounded-md`, `shadow-card`,
9
+ * etc. resolve through the same live-swappable custom properties instead of static values.
10
+ */
11
+ @import "tailwindcss";
12
+
13
+ /*
14
+ * Tailwind v4's automatic content scanning excludes `node_modules` by default, which is exactly where
15
+ * `@rapidmx/react-shared`'s components live from every consumer's perspective (a `link:`-ed sibling) -
16
+ * without this, none of its Tailwind classnames (Modal/Drawer/Button/Alert/Skeleton/FormField/
17
+ * PopoverPortal/ContactAvatar/MiniDatePicker/BottomTabBar) would ever be scanned.
18
+ */
19
+ @source "../../node_modules/@rapidmx/react-shared/src";
20
+
21
+ /*
22
+ * `server`'s own build (unlike `electron-client`, which already explicitly sourced this same tree in
23
+ * its own `styles.css`) has no explicit `@source` for this package's *own* `apps/**` tree at all,
24
+ * relying entirely on Tailwind's "automatic" module-graph-based detection - confirmed empirically to
25
+ * be incomplete here: `server`'s `vite.config.ts` (`@rapidrest/react`'s `createViteConfig`) builds one
26
+ * independent Rollup entry per page (dozens of them, one per `www`/`admin` route), and Tailwind's
27
+ * per-css-module content scan only picked up classnames from whichever ONE chunk's reachable graph
28
+ * `app.css` first got attached to - e.g. every `admin`-only utility (`justify-between`, `px-6`,
29
+ * `max-w-md`, ...) used by `AdminShell.tsx` (which has its own, separate `import "./app.css"`) was
30
+ * silently absent from the built stylesheet, even though `AdminShell.tsx` itself was very much in the
31
+ * bundle - verified directly by diffing every classname `AdminShell.tsx` uses against the actual built
32
+ * CSS. An explicit `@source` here makes the scan independent of Vite's per-entry chunking, the same
33
+ * fix this file already applies to `react-shared`, just for this package's own source too.
34
+ */
35
+ @source "../../";
36
+
37
+ /*
38
+ * Self-hosted display typeface — the four weights shipped in `public/fonts/`. Matches the
39
+ * Book/Medium/Bold/Heavy naming + 400/500/700/800 weight mapping from the brand style guide
40
+ * (https://powerlevel.gg/style-guide/): Blender Pro is for headings/display/short UI labels only —
41
+ * never long-form body copy, which stays on `--font-sans` (Inter) below.
42
+ */
43
+ @font-face {
44
+ font-family: "Blender Pro";
45
+ src: url("/fonts/Blender-Pro-Book.ttf") format("truetype");
46
+ font-weight: 400;
47
+ font-style: normal;
48
+ font-display: swap;
49
+ }
50
+ @font-face {
51
+ font-family: "Blender Pro";
52
+ src: url("/fonts/Blender-Pro-Medium.ttf") format("truetype");
53
+ font-weight: 500;
54
+ font-style: normal;
55
+ font-display: swap;
56
+ }
57
+ @font-face {
58
+ font-family: "Blender Pro";
59
+ src: url("/fonts/Blender-Pro-Bold.ttf") format("truetype");
60
+ font-weight: 700;
61
+ font-style: normal;
62
+ font-display: swap;
63
+ }
64
+ @font-face {
65
+ font-family: "Blender Pro";
66
+ src: url("/fonts/Blender-Pro-Heavy.ttf") format("truetype");
67
+ font-weight: 800;
68
+ font-style: normal;
69
+ font-display: swap;
70
+ }
71
+
72
+ :root {
73
+ /* Teal — the primary structural/brand color (nav, links, active states, focus rings), sampled
74
+ * from the new logo's glass-cube color. Same 7-step shape the old cyan scale used (600/700/800/
75
+ * 900/500/400/300), just re-hued. */
76
+ --rr-color-primary: #0d9488;
77
+ --rr-color-primary-dark: #0f766e;
78
+ --rr-color-primary-darker: #115e59;
79
+ --rr-color-primary-darkest: #134e4a;
80
+ --rr-color-primary-light: #14b8a6;
81
+ --rr-color-primary-lighter: #2dd4bf;
82
+ --rr-color-primary-lightest: #5eead4;
83
+
84
+ /* Gold — the action/highlight color (primary buttons, hover accents), sampled from the new
85
+ * logo's envelope. Values taken directly from the brand style guide's gold palette
86
+ * (https://powerlevel.gg/style-guide/), which is already vetted for WCAG AA contrast. */
87
+ --rr-color-accent: #a3690a;
88
+ --rr-color-accent-dark: #7a4e08;
89
+ --rr-color-accent-soft: #c98a1d;
90
+ --rr-color-accent-pale: #fbe9c2;
91
+ --rr-color-text-on-accent: #1c2526;
92
+
93
+ --rr-color-danger: #dc2626;
94
+ --rr-color-danger-bg: #fef2f2;
95
+ --rr-color-success: #16a34a;
96
+ --rr-color-warning: #f0b429;
97
+ --rr-color-warning-contrast: #1c2526;
98
+
99
+ --rr-color-bg: #ffffff;
100
+ --rr-color-surface: #ffffff;
101
+ --rr-color-surface-alt: #f5f7f8;
102
+ --rr-color-border: #e3e8ea;
103
+ --rr-color-text: #1c2526;
104
+ --rr-color-text-muted: #5a6b6e;
105
+ --rr-color-text-on-primary: #ffffff;
106
+
107
+ --rr-radius-sm: 0.4rem;
108
+ --rr-radius-md: 0.8rem;
109
+ --rr-radius-lg: 1rem;
110
+ --rr-radius-pill: 999px;
111
+
112
+ --rr-shadow-lw: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
113
+ --rr-shadow-md: 0 5px 40px rgba(0, 0, 0, 0.12);
114
+
115
+ /* Inter (loaded via Google Fonts in each app's `_layout.tsx`) is body copy's typeface per the
116
+ * style guide; `--rr-font-display` (Blender Pro) is applied separately, to headings/short UI
117
+ * labels only — see the global `h1`-`h6` rule near the bottom of this file. */
118
+ --rr-font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans",
119
+ sans-serif, BlinkMacSystemFont;
120
+ --rr-font-display: "Blender Pro", var(--rr-font-family);
121
+
122
+ color-scheme: light;
123
+ }
124
+
125
+ @media (prefers-color-scheme: dark) {
126
+ :root:not([data-theme="light"]) {
127
+ --rr-color-primary: #2dd4bf;
128
+ --rr-color-primary-dark: #14b8a6;
129
+ --rr-color-primary-darker: #0d9488;
130
+ --rr-color-primary-darkest: #0f766e;
131
+ --rr-color-primary-light: #5eead4;
132
+ --rr-color-primary-lighter: #99f6e4;
133
+ --rr-color-primary-lightest: #ccfbf1;
134
+
135
+ --rr-color-accent: #f2af0d;
136
+ --rr-color-accent-dark: #c98a0a;
137
+ --rr-color-accent-soft: #f6ca5f;
138
+ --rr-color-accent-pale: #fce9ba;
139
+ --rr-color-text-on-accent: #1c2526;
140
+
141
+ --rr-color-danger: #f87171;
142
+ --rr-color-danger-bg: #3f1d1d;
143
+ --rr-color-success: #4ade80;
144
+ --rr-color-warning: #fbbf24;
145
+ --rr-color-warning-contrast: #1c2526;
146
+
147
+ --rr-color-bg: #14181a;
148
+ --rr-color-surface: #1b2022;
149
+ --rr-color-surface-alt: #20262a;
150
+ --rr-color-border: #313a3d;
151
+ --rr-color-text: #eef2f3;
152
+ --rr-color-text-muted: #9fb0b3;
153
+ --rr-color-text-on-primary: #0b1416;
154
+
155
+ color-scheme: dark;
156
+ }
157
+ }
158
+
159
+ :root[data-theme="dark"] {
160
+ --rr-color-primary: #2dd4bf;
161
+ --rr-color-primary-dark: #14b8a6;
162
+ --rr-color-primary-darker: #0d9488;
163
+ --rr-color-primary-darkest: #0f766e;
164
+ --rr-color-primary-light: #5eead4;
165
+ --rr-color-primary-lighter: #99f6e4;
166
+ --rr-color-primary-lightest: #ccfbf1;
167
+
168
+ --rr-color-accent: #f2af0d;
169
+ --rr-color-accent-dark: #c98a0a;
170
+ --rr-color-accent-soft: #f6ca5f;
171
+ --rr-color-accent-pale: #fce9ba;
172
+ --rr-color-text-on-accent: #1c2526;
173
+
174
+ --rr-color-danger: #f87171;
175
+ --rr-color-danger-bg: #3f1d1d;
176
+ --rr-color-success: #4ade80;
177
+ --rr-color-warning: #fbbf24;
178
+ --rr-color-warning-contrast: #1c2526;
179
+
180
+ --rr-color-bg: #14181a;
181
+ --rr-color-surface: #1b2022;
182
+ --rr-color-surface-alt: #20262a;
183
+ --rr-color-border: #313a3d;
184
+ --rr-color-text: #eef2f3;
185
+ --rr-color-text-muted: #9fb0b3;
186
+ --rr-color-text-on-primary: #0b1416;
187
+
188
+ color-scheme: dark;
189
+ }
190
+
191
+ @theme {
192
+ --color-primary: var(--rr-color-primary);
193
+ --color-primary-dark: var(--rr-color-primary-dark);
194
+ --color-primary-darker: var(--rr-color-primary-darker);
195
+ --color-primary-darkest: var(--rr-color-primary-darkest);
196
+ --color-primary-light: var(--rr-color-primary-light);
197
+ --color-primary-lighter: var(--rr-color-primary-lighter);
198
+ --color-primary-lightest: var(--rr-color-primary-lightest);
199
+ --color-accent: var(--rr-color-accent);
200
+ --color-accent-dark: var(--rr-color-accent-dark);
201
+ --color-accent-soft: var(--rr-color-accent-soft);
202
+ --color-accent-pale: var(--rr-color-accent-pale);
203
+ --color-text-on-accent: var(--rr-color-text-on-accent);
204
+
205
+ --color-danger: var(--rr-color-danger);
206
+ --color-danger-bg: var(--rr-color-danger-bg);
207
+ --color-success: var(--rr-color-success);
208
+ --color-warning: var(--rr-color-warning);
209
+ --color-warning-contrast: var(--rr-color-warning-contrast);
210
+
211
+ --color-bg: var(--rr-color-bg);
212
+ --color-surface: var(--rr-color-surface);
213
+ --color-surface-alt: var(--rr-color-surface-alt);
214
+ --color-border: var(--rr-color-border);
215
+ --color-text: var(--rr-color-text);
216
+ --color-text-muted: var(--rr-color-text-muted);
217
+ --color-text-on-primary: var(--rr-color-text-on-primary);
218
+
219
+ --radius-sm: var(--rr-radius-sm);
220
+ --radius-md: var(--rr-radius-md);
221
+ --radius-lg: var(--rr-radius-lg);
222
+ --radius-pill: var(--rr-radius-pill);
223
+
224
+ --shadow-card: var(--rr-shadow-lw);
225
+ --shadow-modal: var(--rr-shadow-md);
226
+
227
+ --font-sans: var(--rr-font-family);
228
+ --font-display: var(--rr-font-display);
229
+ }
230
+
231
+ *,
232
+ *::before,
233
+ *::after {
234
+ box-sizing: border-box;
235
+ }
236
+
237
+ html,
238
+ body {
239
+ margin: 0;
240
+ padding: 0;
241
+ }
242
+
243
+ body {
244
+ font-family: var(--rr-font-family);
245
+ background: var(--rr-color-bg);
246
+ color: var(--rr-color-text);
247
+ line-height: 1.5;
248
+ -webkit-font-smoothing: antialiased;
249
+ }
250
+
251
+ /* Blender Pro is the brand's display typeface (see the style guide linked at the top of this file) —
252
+ * applied to every heading regardless of its content, since a font swap alone never hurts even
253
+ * dynamic content (a contact's name, an email subject). Uppercase/wide-tracking treatment is
254
+ * deliberately NOT applied blanket here — it reads fine on short, static UI chrome ("Mailboxes",
255
+ * "Tasks") but turns a real name or subject line into shouty all-caps, so that's opted into per
256
+ * element instead (`uppercase tracking-wide`), matching the style guide's own body-text warning
257
+ * ("Blender Pro set in long paragraphs reads as shouting") applied to short user content too. */
258
+ h1,
259
+ h2,
260
+ h3,
261
+ h4,
262
+ h5,
263
+ h6 {
264
+ font-family: var(--rr-font-display);
265
+ }
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function NotFoundPage() {
3
+ return _jsx("p", { children: "Page not found" });
4
+ }
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export default function ErrorPage() {
3
+ return _jsx("p", { children: "Internal server error" });
4
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CUSTOM_STYLESHEET_LINK_ID } from "@rapidmx/react-shared/branding/useBranding.js";
3
+ export default function Layout({ children, branding }) {
4
+ const title = branding?.title || branding?.companyName ? `${branding?.title || branding?.companyName}: Mail` : "RapidMX: Mail";
5
+ const iconHref = branding?.iconUrl || branding?.logoUrl || "/images/logo.svg";
6
+ const stylesheetHref = branding?.stylesheetUrl;
7
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title }), _jsx("link", { rel: "icon", type: "image/svg+xml", href: iconHref }), _jsx("link", { rel: "alternate icon", href: "/favicon.ico" }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), _jsx("link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" }), stylesheetHref && _jsx("link", { rel: "stylesheet", href: stylesheetHref, id: CUSTOM_STYLESHEET_LINK_ID })] }), _jsx("body", { children: children })] }));
8
+ }
@@ -0,0 +1,228 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
8
+ import { HiOutlineBars3 } from "react-icons/hi2";
9
+ import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
10
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
11
+ import { colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
12
+ import { listCalendarEvents } from "@rapidmx/react-shared/calendar/calendarApi.js";
13
+ import { moveOccurrence, resizeOccurrenceEnd } from "@rapidmx/react-shared/calendar/calendarMutations.js";
14
+ import { resolveDragAction } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
15
+ import { createFolder } from "@rapidmx/react-shared/mail/mailApi.js";
16
+ import { expandAllOccurrences } from "@rapidmx/react-shared/calendar/recurrence.js";
17
+ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
18
+ import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
19
+ import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
20
+ import EventModal from "../../shared/components/calendar/EventModal.js";
21
+ import MiniDatePicker from "@rapidmx/react-shared/components/pickers/MiniDatePicker.js";
22
+ import MonthView from "../../shared/components/calendar/MonthView.js";
23
+ import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
24
+ import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
25
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
26
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
27
+ export default function CalendarPage(props) {
28
+ return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, {}) }));
29
+ }
30
+ const VIEW_TYPES = ["month", "week", "workWeek", "day", "split"];
31
+ const VIEW_LABELS = { month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
32
+ function CalendarContent() {
33
+ const { mailboxUid, folderUid, calendarFolders, mailboxes, reloadFolders } = useCalendarShell();
34
+ // `CalendarShell` only ever renders this component once `mailboxUid` is set, and always to a value
35
+ // drawn from `mailboxes` itself (see its own resolution logic) — the lookup below always succeeds.
36
+ const organizerAddress = mailboxes.find((mb) => mb.uid === mailboxUid).primarySmtpAddress;
37
+ // `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
38
+ // scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
39
+ // immediately on a deliberate movement, no change from before) + `TouchSensor` (a `delay`+`tolerance`
40
+ // — a touch drag only activates after a brief press-and-hold, so a quick swipe scrolls normally) is
41
+ // dnd-kit's own documented pattern for this exact conflict.
42
+ const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } }));
43
+ const [drawerOpen, setDrawerOpen] = useState(false);
44
+ const [view, setView] = useState("month");
45
+ const [viewDate, setViewDate] = useState(() => startOfDay(new Date()));
46
+ const [events, setEvents] = useState([]);
47
+ const [loading, setLoading] = useState(true);
48
+ const [error, setError] = useState(null);
49
+ const [actionError, setActionError] = useState(null);
50
+ const [modal, setModal] = useState(null);
51
+ // `null` means "not yet customized by the user" — every calendar is treated as checked without
52
+ // needing to be seeded from `calendarFolders` the moment it loads (see `checkedFolderUids` below).
53
+ // Once the user toggles anything, this becomes a real (possibly empty) set of exactly what's checked.
54
+ const [checkedFolderUidsState, setCheckedFolderUidsState] = useState(null);
55
+ useEffect(() => {
56
+ const params = new URLSearchParams(window.location.search);
57
+ const requestedView = params.get("view");
58
+ if (VIEW_TYPES.includes(requestedView ?? "")) {
59
+ setView(requestedView);
60
+ }
61
+ const requestedDate = params.get("date");
62
+ const parsedDate = requestedDate ? new Date(requestedDate) : null;
63
+ if (parsedDate && !isNaN(parsedDate.getTime())) {
64
+ setViewDate(startOfDay(parsedDate));
65
+ }
66
+ }, []);
67
+ const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
68
+ const folderColors = useMemo(() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorForFolder(f)])), [calendarFolders]);
69
+ function toggleCalendar(toggledUid) {
70
+ setCheckedFolderUidsState((prev) => {
71
+ const next = new Set(prev ?? calendarFolders.map((f) => f.uid));
72
+ if (next.has(toggledUid)) {
73
+ next.delete(toggledUid);
74
+ }
75
+ else {
76
+ next.add(toggledUid);
77
+ }
78
+ return next;
79
+ });
80
+ }
81
+ async function handleAddCalendar(name, color) {
82
+ // `CalendarContent` only renders once `mailboxUid` is resolved — same invariant as `organizerAddress` above.
83
+ const created = await createFolder({ mailboxUid: mailboxUid, name, type: "calendar", color });
84
+ setCheckedFolderUidsState((prev) => new Set([...(prev ?? calendarFolders.map((f) => f.uid)), created.uid]));
85
+ reloadFolders();
86
+ }
87
+ const { rangeStart, rangeEnd, days } = useMemo(() => {
88
+ if (view === "month") {
89
+ const start = startOfWeek(startOfMonth(viewDate), { weekStartsOn: 1 });
90
+ const end = endOfWeek(endOfMonth(viewDate), { weekStartsOn: 1 });
91
+ return { rangeStart: start, rangeEnd: end, days: [] };
92
+ }
93
+ if (view === "week") {
94
+ const start = startOfWeek(viewDate, { weekStartsOn: 1 });
95
+ const end = endOfWeek(viewDate, { weekStartsOn: 1 });
96
+ return { rangeStart: start, rangeEnd: end, days: eachDayOfInterval({ start, end }) };
97
+ }
98
+ if (view === "workWeek") {
99
+ const start = startOfWeek(viewDate, { weekStartsOn: 1 });
100
+ const weekdays = eachDayOfInterval({ start, end: endOfWeek(viewDate, { weekStartsOn: 1 }) }).slice(0, 5);
101
+ return { rangeStart: start, rangeEnd: endOfDay(weekdays[weekdays.length - 1]), days: weekdays };
102
+ }
103
+ // "day" and "split" both show a single day, just laid out differently below.
104
+ const start = startOfDay(viewDate);
105
+ const end = endOfDay(viewDate);
106
+ return { rangeStart: start, rangeEnd: end, days: [start] };
107
+ }, [view, viewDate]);
108
+ function reload() {
109
+ const targets = calendarFolders.filter((f) => checkedFolderUids.has(f.uid));
110
+ if (targets.length === 0) {
111
+ setEvents([]);
112
+ setLoading(false);
113
+ return;
114
+ }
115
+ setLoading(true);
116
+ setError(null);
117
+ void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid)))
118
+ .then((results) => {
119
+ const loaded = [];
120
+ const failures = [];
121
+ results.forEach((result, i) => {
122
+ if (result.status === "fulfilled") {
123
+ loaded.push(...result.value);
124
+ }
125
+ else {
126
+ failures.push({ name: targets[i].name, err: result.reason });
127
+ }
128
+ });
129
+ setEvents(loaded);
130
+ if (failures.length === 0) {
131
+ return;
132
+ }
133
+ // A single calendar's failure (by far the common case — most mailboxes have exactly one)
134
+ // surfaces that calendar's own error message unchanged, matching this page's original
135
+ // single-calendar behavior exactly; only a genuine multi-calendar partial failure falls
136
+ // back to a combined message naming which calendars didn't load.
137
+ if (failures.length === 1 && targets.length === 1) {
138
+ const err = failures[0].err;
139
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your calendar.");
140
+ }
141
+ else {
142
+ setError(`Could not load events for: ${failures.map((f) => f.name).join(", ")}.`);
143
+ }
144
+ })
145
+ .finally(() => setLoading(false));
146
+ }
147
+ // Keyed on stable string summaries (not the array/set objects themselves, which are new references
148
+ // every render) so this only re-fires when the actual set of folders to fetch changes.
149
+ const calendarFolderUidsKey = calendarFolders.map((f) => f.uid).sort().join(",");
150
+ const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
151
+ useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
152
+ const occurrences = useMemo(() => expandAllOccurrences(events, rangeStart, rangeEnd), [events, rangeStart, rangeEnd]);
153
+ const splitColumns = useMemo(() => calendarFolders
154
+ .filter((f) => checkedFolderUids.has(f.uid))
155
+ .map((f) => ({ folderUid: f.uid, name: f.name, color: colorForFolder(f) })), [calendarFolders, checkedFolderUidsKey]);
156
+ function shiftView(direction) {
157
+ setViewDate((d) => view === "month"
158
+ ? addMonths(d, direction)
159
+ : view === "week" || view === "workWeek"
160
+ ? addWeeks(d, direction)
161
+ : addDays(d, direction));
162
+ }
163
+ function goToday() {
164
+ setViewDate(startOfDay(new Date()));
165
+ }
166
+ function handleSelectDay(date) {
167
+ setView("day");
168
+ setViewDate(startOfDay(date));
169
+ }
170
+ function openNewEvent(start, end, targetFolderUid) {
171
+ if (!mailboxUid || !folderUid) {
172
+ return;
173
+ }
174
+ setModal({
175
+ occurrence: null,
176
+ initialStart: start ?? new Date(),
177
+ initialEnd: end ?? new Date(Date.now() + 30 * 60000),
178
+ targetFolderUid,
179
+ });
180
+ }
181
+ function openEvent(occurrence) {
182
+ setModal({ occurrence });
183
+ }
184
+ function closeModal() {
185
+ setModal(null);
186
+ }
187
+ function handleSaved() {
188
+ setModal(null);
189
+ reload();
190
+ }
191
+ function handleDeleted() {
192
+ setModal(null);
193
+ reload();
194
+ }
195
+ async function handleDragEnd(event) {
196
+ const overId = event.over ? String(event.over.id) : undefined;
197
+ const action = resolveDragAction(String(event.active.id), overId, occurrences);
198
+ if (!action) {
199
+ return;
200
+ }
201
+ setActionError(null);
202
+ try {
203
+ if (action.type === "move") {
204
+ await moveOccurrence(action.occurrence, action.deltaMs);
205
+ }
206
+ else {
207
+ await resizeOccurrenceEnd(action.occurrence, action.newEnd);
208
+ }
209
+ reload();
210
+ }
211
+ catch (err) {
212
+ setActionError(err instanceof ApiRequestError ? err.message : "Could not update this event.");
213
+ }
214
+ }
215
+ const title = view === "month"
216
+ ? format(viewDate, "MMMM yyyy")
217
+ : view === "week" || view === "workWeek"
218
+ ? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
219
+ : format(viewDate, "EEEE, MMMM d, yyyy");
220
+ const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => setViewDate(startOfDay(date)) }), _jsx(CalendarListSidebar, { calendars: calendarFolders, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar })] }));
221
+ return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx("div", { className: "hidden md:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex items-center gap-3 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "md:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(Button, { type: "button", onClick: () => openNewEvent(), className: "!w-auto shrink-0", children: "+ New event" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx("div", { className: "ml-auto flex gap-1 overflow-x-auto", children: VIEW_TYPES.map((v) => (_jsx("button", { type: "button", onClick: () => setView(v), className: [
222
+ "px-3 h-7 text-sm rounded-sm whitespace-nowrap",
223
+ view === v ? "bg-primary text-white" : "hover:bg-surface-alt",
224
+ (v === "workWeek" || v === "split") && "hidden md:inline-block",
225
+ ]
226
+ .filter(Boolean)
227
+ .join(" "), children: VIEW_LABELS[v] }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), actionError && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: actionError }) })), loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid) => openNewEvent(start, end, targetFolderUid) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: openNewEvent })) })), modal && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: mailboxUid, folderUid: modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid, calendars: calendarFolders.map((f) => ({ uid: f.uid, name: f.name })), organizerAddress: organizerAddress, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, onSaved: handleSaved, onDeleted: handleDeleted }))] })] }));
228
+ }
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { deleteContact, getContact } from "@rapidmx/react-shared/contacts/contactsApi.js";
9
+ import ContactsShell from "../../shared/components/contacts/layout/ContactsShell.js";
10
+ import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
11
+ import ContactForm from "../../shared/components/contacts/ContactForm.js";
12
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
13
+ /**
14
+ * Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
15
+ * existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
16
+ * equivalent "new contact" route — an unsaved contact has no uid to route on, so creation stays an
17
+ * in-place mode-switch on every device (see `apps/www/contacts/index.tsx`'s `handleNew`).
18
+ */
19
+ export default function ContactDetailPage(props) {
20
+ return (_jsx(ContactsShell, { ...props, children: _jsx(ContactDetailContent, { uid: props.params.uid }) }));
21
+ }
22
+ function ContactDetailContent({ uid }) {
23
+ const [contact, setContact] = useState(null);
24
+ const [loading, setLoading] = useState(true);
25
+ const [error, setError] = useState(null);
26
+ const [mode, setMode] = useState("view");
27
+ useEffect(() => {
28
+ setLoading(true);
29
+ setError(null);
30
+ getContact(uid)
31
+ .then(setContact)
32
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this contact."))
33
+ .finally(() => setLoading(false));
34
+ }, [uid]);
35
+ // Takes `contact` as a parameter rather than reading it from the closure/state — mirrors the desktop
36
+ // pane's own `handleDelete` in `apps/www/contacts/index.tsx`. The only caller (below) is only ever
37
+ // rendered once `contact` is already resolved (see the `error || !contact` guard above it), so this
38
+ // never runs with a stale/absent contact — no redundant null check needed here.
39
+ async function handleDelete(contact) {
40
+ try {
41
+ await deleteContact(contact.uid, contact.version);
42
+ window.location.href = "/contacts";
43
+ }
44
+ catch (err) {
45
+ setError(err instanceof ApiRequestError ? err.message : "Could not delete this contact.");
46
+ }
47
+ }
48
+ if (loading) {
49
+ return _jsx("p", { className: "p-8 text-sm text-text-muted", children: "Loading\u2026" });
50
+ }
51
+ if (error || !contact) {
52
+ return _jsx(Alert, { children: error ?? "Contact not found." });
53
+ }
54
+ const backHref = "/contacts";
55
+ if (mode === "edit") {
56
+ return (_jsxs("div", { className: "p-6", children: [_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block mb-4", children: "\u2190 Back to contacts" }), _jsx(ContactForm, { contact: contact, onSaved: (saved) => {
57
+ setContact(saved);
58
+ setMode("view");
59
+ }, onCancel: () => setMode("view") })] }));
60
+ }
61
+ return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref }) }));
62
+ }