@tribe-nest/forge 3.31.0 → 3.34.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/package.json +7 -2
- package/src/contexts/CartContext.tsx +17 -1
- package/src/contexts/_tests/CartContext.spec.tsx +36 -0
- package/src/data/queries/useMyBookings.ts +9 -0
- package/src/i18n/_tests/translationKeys.spec.ts +15 -0
- package/src/i18n/de.json +40 -2
- package/src/i18n/en.json +40 -2
- package/src/ui/index.ts +25 -0
- package/src/ui/media/BookingCallScreen.tsx +33 -0
- package/src/ui/media/CallStage.tsx +153 -62
- package/src/ui/media/_tests/CallStage.spec.tsx +265 -19
- package/src/ui/media/_tests/bookingSession.spec.tsx +48 -0
- package/src/ui/media/_tests/callState.spec.ts +63 -16
- package/src/ui/media/bookingSession.tsx +45 -57
- package/src/ui/media/bookingWindow.ts +81 -0
- package/src/ui/media/callState.ts +42 -23
- package/src/ui/styled/AccountDashboard.tsx +106 -6
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +166 -0
- package/src/ui/styled/forge-utilities.css +832 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tribe-nest/forge",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
"./ui": "./src/ui/index.ts",
|
|
11
11
|
"./ui/headless": "./src/ui/headless/index.ts",
|
|
12
12
|
"./server": "./src/server/index.ts",
|
|
13
|
-
"./media": "./src/ui/media/index.ts"
|
|
13
|
+
"./media": "./src/ui/media/index.ts",
|
|
14
|
+
"./forge-utilities.css": "./src/ui/styled/forge-utilities.css"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"src"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
20
|
+
"build:css": "tailwindcss -i tailwind.input.css -o src/ui/styled/forge-utilities.css && node scripts/wrapForgeCss.mjs",
|
|
21
|
+
"prepack": "npm run build:css",
|
|
19
22
|
"check-types": "tsc --noEmit",
|
|
20
23
|
"test": "vitest run",
|
|
21
24
|
"test:watch": "vitest"
|
|
@@ -41,8 +44,10 @@
|
|
|
41
44
|
"react-dom": ">=18"
|
|
42
45
|
},
|
|
43
46
|
"devDependencies": {
|
|
47
|
+
"@tailwindcss/cli": "4.1.11",
|
|
44
48
|
"@types/react": "^19.1.2",
|
|
45
49
|
"simple-icons": "^16.28.0",
|
|
50
|
+
"tailwindcss": "4.1.11",
|
|
46
51
|
"typescript": "~5.8.3"
|
|
47
52
|
},
|
|
48
53
|
"license": "ISC"
|
|
@@ -227,6 +227,23 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
|
|
227
227
|
i.recipientEmail === item.recipientEmail,
|
|
228
228
|
);
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The drawer opens on EVERY add, including one that turns out to be a
|
|
232
|
+
* duplicate.
|
|
233
|
+
*
|
|
234
|
+
* It used to open only for a line that was new. Pressing Add to cart on
|
|
235
|
+
* something already in the cart updated the line silently and left the
|
|
236
|
+
* page exactly as it was, which reads as a dead button: the shopper
|
|
237
|
+
* cannot tell the difference between "already there" and "the click did
|
|
238
|
+
* not register", so they press it again. Opening the drawer answers the
|
|
239
|
+
* question the press was asking, which is "is this in my cart".
|
|
240
|
+
*
|
|
241
|
+
* The RETURN value still distinguishes the two cases, so a consumer that
|
|
242
|
+
* wants to say "already in your cart" can still do it. Only the drawer
|
|
243
|
+
* stopped depending on which case it was.
|
|
244
|
+
*/
|
|
245
|
+
setCartOpen(true);
|
|
246
|
+
|
|
230
247
|
if (exists) {
|
|
231
248
|
setCartItems((prev) =>
|
|
232
249
|
prev.map((i) => {
|
|
@@ -247,7 +264,6 @@ export function CartProvider({ children }: { children: ReactNode }) {
|
|
|
247
264
|
return false;
|
|
248
265
|
}
|
|
249
266
|
setCartItems((prev) => [...prev, next]);
|
|
250
|
-
setCartOpen(true);
|
|
251
267
|
return true;
|
|
252
268
|
},
|
|
253
269
|
[cartItems],
|
|
@@ -147,6 +147,42 @@ describe("restoreCart: tickets", () => {
|
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
describe("addToCart: the drawer", () => {
|
|
151
|
+
it("opens the drawer for a NEW line", () => {
|
|
152
|
+
const { result } = mount();
|
|
153
|
+
|
|
154
|
+
act(() => void result.current.addToCart(item()));
|
|
155
|
+
|
|
156
|
+
expect(result.current.isCartOpen).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("opens the drawer again for a line ALREADY in the cart", () => {
|
|
160
|
+
/**
|
|
161
|
+
* The regression this exists for. Adding something already in the cart
|
|
162
|
+
* updated the line and left the page untouched, so the button read as
|
|
163
|
+
* dead: nothing on screen distinguished "already in your cart" from "that
|
|
164
|
+
* click did nothing", and the shopper pressed it again.
|
|
165
|
+
*/
|
|
166
|
+
const { result } = mount();
|
|
167
|
+
|
|
168
|
+
act(() => void result.current.addToCart(item()));
|
|
169
|
+
act(() => result.current.setCartOpen(false));
|
|
170
|
+
expect(result.current.isCartOpen).toBe(false);
|
|
171
|
+
|
|
172
|
+
let wasNew: boolean | undefined;
|
|
173
|
+
act(() => {
|
|
174
|
+
wasNew = result.current.addToCart(item({ quantity: 2 }));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result.current.isCartOpen).toBe(true);
|
|
178
|
+
// Still one line, still reporting the duplicate through the return value,
|
|
179
|
+
// so a consumer that wants to say "already in your cart" can.
|
|
180
|
+
expect(result.current.cartItems).toHaveLength(1);
|
|
181
|
+
expect(result.current.cartItems[0]).toMatchObject({ quantity: 2 });
|
|
182
|
+
expect(wasNew).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
150
186
|
describe("restoreCart: products", () => {
|
|
151
187
|
it("replaces a matching line rather than appending a duplicate of it", () => {
|
|
152
188
|
const { result } = mount();
|
|
@@ -93,6 +93,15 @@ export type MyBooking = {
|
|
|
93
93
|
/** What became of the money: `refunded`, `voided_free`, `manual_refund_required`, … */
|
|
94
94
|
cancellationOutcome: string | null;
|
|
95
95
|
createdAt: string;
|
|
96
|
+
/**
|
|
97
|
+
* The call's address, or null when this session has no call.
|
|
98
|
+
*
|
|
99
|
+
* Present only for a CONFIRMED session on video, because that is when the
|
|
100
|
+
* room record is written. It is the id in `/video-calls/:callId`, opaque on
|
|
101
|
+
* purpose: it is not derived from the booking id, so a link in an email
|
|
102
|
+
* reveals nothing about the booking behind it.
|
|
103
|
+
*/
|
|
104
|
+
videoCallId?: string | null;
|
|
96
105
|
/** The slot the booking currently points at — what a reschedule moves. */
|
|
97
106
|
coachingBookingSlotId: string;
|
|
98
107
|
/**
|
|
@@ -150,4 +150,19 @@ describe("Forge i18n: every key a component uses exists in every bundle", () =>
|
|
|
150
150
|
}
|
|
151
151
|
expect(mismatches, `Interpolation drift:\n${mismatches.join("\n")}`).toEqual([]);
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
it("no bundle value uses i18next's double braces, which Forge's interpolator does not read", () => {
|
|
155
|
+
// `translateForge` substitutes `{name}`. On `{{date}}` the regex matches
|
|
156
|
+
// the INNER braces, so the visitor saw "{Aug 20, 2026}" with a stray pair
|
|
157
|
+
// of braces around the value. Two keys shipped that way (copied from the
|
|
158
|
+
// admin bundle, which is i18next). The convention is single braces, and
|
|
159
|
+
// this pins it for every value in every bundle.
|
|
160
|
+
const offenders: string[] = [];
|
|
161
|
+
for (const [lang, bundle] of Object.entries(LOCALES)) {
|
|
162
|
+
for (const [key, value] of Object.entries(bundle)) {
|
|
163
|
+
if (value.includes("{{") || value.includes("}}")) offenders.push(`${lang}: ${key}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
expect(offenders, `Double-brace placeholders:\n${offenders.join("\n")}`).toEqual([]);
|
|
167
|
+
});
|
|
153
168
|
});
|
package/src/i18n/de.json
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
"forge.account_dashboard.booking_duration": "{minutes} Minuten",
|
|
6
6
|
"forge.account_dashboard.booking_join_label": "Teilnehmen:",
|
|
7
7
|
"forge.account_dashboard.booking_video_call": "Videoanruf auf dieser Website",
|
|
8
|
+
"forge.account_dashboard.booking_join_video": "Videoanruf beitreten",
|
|
9
|
+
"forge.account_dashboard.booking_video_opens_at": "Videoanruf: Der Raum öffnet {datetime}",
|
|
10
|
+
"forge.account_dashboard.booking_video_closed": "Videoanruf: Der Raum dieser Session ist geschlossen",
|
|
8
11
|
"forge.account_dashboard.booking_where_label": "Wo:",
|
|
9
12
|
"forge.account_dashboard.cancel": "Abbrechen",
|
|
10
13
|
"forge.account_dashboard.cancel_confirm_confirm": "Mitgliedschaft kündigen",
|
|
11
14
|
"forge.account_dashboard.cancel_confirm_free": "Ihre Vorteile enden sofort.",
|
|
12
15
|
"forge.account_dashboard.cancel_confirm_keep": "Mitgliedschaft behalten",
|
|
13
16
|
"forge.account_dashboard.cancel_confirm_paid": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum Ende des bezahlten Zeitraums bestehen.",
|
|
14
|
-
"forge.account_dashboard.cancel_confirm_paid_until": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum {
|
|
17
|
+
"forge.account_dashboard.cancel_confirm_paid_until": "Sie wird nicht verlängert, und Ihre Vorteile bleiben bis zum {date} bestehen.",
|
|
15
18
|
"forge.account_dashboard.cancel_confirm_title": "Mitgliedschaft kündigen?",
|
|
16
19
|
"forge.account_dashboard.cancel_deletion": "Löschung abbrechen",
|
|
17
20
|
"forge.account_dashboard.cancel_session": "Session stornieren",
|
|
@@ -184,6 +187,41 @@
|
|
|
184
187
|
"forge.bundle_confirmation.tickets_emailed": "Tickets per E-Mail an dich unterwegs",
|
|
185
188
|
"forge.bundle_confirmation.title": "Alles erledigt",
|
|
186
189
|
"forge.bundle_confirmation.view_orders": "Bestellungen ansehen",
|
|
190
|
+
"forge.call_stage.status_ended": "Der Anruf ist beendet",
|
|
191
|
+
"forge.call_stage.status_connected": "Verbunden",
|
|
192
|
+
"forge.call_stage.status_connecting": "Verbindung zum Anruf wird hergestellt",
|
|
193
|
+
"forge.call_stage.status_left": "Du hast den Anruf verlassen",
|
|
194
|
+
"forge.call_stage.status_refused": "Beitritt zu diesem Anruf nicht möglich",
|
|
195
|
+
"forge.call_stage.status_moving": "Du wirst auf einen anderen Server verschoben",
|
|
196
|
+
"forge.call_stage.status_moving_detail": "Das dauert ein paar Sekunden, und niemand sonst wird getrennt.",
|
|
197
|
+
"forge.call_stage.status_move_failed": "Du konntest nicht auf einen anderen Server verschoben werden",
|
|
198
|
+
"forge.call_stage.status_move_failed_detail": "Niemand sonst wurde getrennt. Versuche es erneut, um wieder beizutreten.",
|
|
199
|
+
"forge.call_stage.status_reconnecting": "Verbindung unterbrochen, wird wiederhergestellt",
|
|
200
|
+
"forge.call_stage.status_reconnecting_detail": "Alle anderen sind weiterhin im Anruf.",
|
|
201
|
+
"forge.call_stage.status_lost": "Verbindung unterbrochen",
|
|
202
|
+
"forge.call_stage.status_lost_detail": "Alle anderen sind weiterhin im Anruf. Versuche es erneut, um wieder beizutreten.",
|
|
203
|
+
"forge.call_stage.retry": "Erneut versuchen",
|
|
204
|
+
"forge.call_stage.recording": "Dieser Anruf wird aufgezeichnet",
|
|
205
|
+
"forge.call_stage.head_count": "In diesem Anruf: {count}",
|
|
206
|
+
"forge.call_stage.help_hear_not_see_label": "Warum höre ich jemanden, sehe ihn aber nicht?",
|
|
207
|
+
"forge.call_stage.help_hear_not_see": "Eine Kachel mit Namen, aber ohne Bild bedeutet, dass diese Person ihre Kamera ausgeschaltet hat oder dass der Anruf so voll ist, dass nur die gerade sprechenden Personen in voller Größe übertragen werden. Beides ist normal, und der Ton ist davon nicht betroffen.",
|
|
208
|
+
"forge.call_stage.listening": "(hört zu)",
|
|
209
|
+
"forge.call_stage.you": "Du",
|
|
210
|
+
"forge.call_stage.you_not_connected": "Du (noch nicht verbunden)",
|
|
211
|
+
"forge.call_stage.mute": "Stummschalten",
|
|
212
|
+
"forge.call_stage.unmute": "Stummschaltung aufheben",
|
|
213
|
+
"forge.call_stage.camera_start": "Kamera starten",
|
|
214
|
+
"forge.call_stage.camera_stop": "Kamera stoppen",
|
|
215
|
+
"forge.call_stage.help_camera_stop_label": "Was macht „Kamera stoppen“?",
|
|
216
|
+
"forge.call_stage.help_camera_stop": "Dein Bild wird nicht mehr gesendet: Die anderen sehen stattdessen deinen Namen. Der Browser hält die Kamera geöffnet, damit das erneute Starten sofort geht und nicht ein zweites Mal um Erlaubnis fragt. Deshalb kann die Kameraleuchte anbleiben, bis du den Anruf verlässt.",
|
|
217
|
+
"forge.call_stage.screen_start": "Bildschirm teilen",
|
|
218
|
+
"forge.call_stage.screen_stop": "Teilen beenden",
|
|
219
|
+
"forge.call_stage.help_screen_label": "Was wird beim Teilen meines Bildschirms gesendet?",
|
|
220
|
+
"forge.call_stage.help_screen": "Was auch immer du im Browser-Dialog auswählst, live an alle im Anruf. Deine Kamera läuft dabei weiter, statt ersetzt zu werden, sodass dich die anderen weiterhin sehen können, während du präsentierst.",
|
|
221
|
+
"forge.call_stage.leave": "Verlassen",
|
|
222
|
+
"forge.call_stage.lost_microphone": "Dein Mikrofon wurde ausgeschaltet, als die Verbindung abbrach. Hebe die Stummschaltung auf, um wieder gehört zu werden.",
|
|
223
|
+
"forge.call_stage.lost_camera": "Deine Kamera wurde ausgeschaltet, als die Verbindung abbrach. Starte sie erneut, um wieder gesehen zu werden.",
|
|
224
|
+
"forge.call_stage.lost_screen": "Deine Bildschirmfreigabe wurde beendet, als die Verbindung abbrach. Teile ihn erneut, um fortzufahren.",
|
|
187
225
|
"forge.cancellation_terms.title": "Stornobedingungen",
|
|
188
226
|
"forge.cart.aria_close": "Warenkorb schließen",
|
|
189
227
|
"forge.cart.aria_open": "Warenkorb öffnen",
|
|
@@ -790,7 +828,7 @@
|
|
|
790
828
|
"forge.membership_checkout.payment_title": "Zahlung",
|
|
791
829
|
"forge.membership_checkout.per_month": "pro Monat",
|
|
792
830
|
"forge.membership_checkout.per_year": "pro Jahr",
|
|
793
|
-
"forge.membership_checkout.pwyw_charged_as": "Ihnen werden {
|
|
831
|
+
"forge.membership_checkout.pwyw_charged_as": "Ihnen werden {amount} berechnet.",
|
|
794
832
|
"forge.membership_checkout.pwyw_label": "Wie viel möchtest du zahlen?",
|
|
795
833
|
"forge.membership_checkout.pwyw_min": "(mindestens {amount})",
|
|
796
834
|
"forge.membership_checkout.select_tier": "Diesen Tarif wählen",
|
package/src/i18n/en.json
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
"forge.account_dashboard.booking_duration": "{minutes} minutes",
|
|
6
6
|
"forge.account_dashboard.booking_join_label": "Join:",
|
|
7
7
|
"forge.account_dashboard.booking_video_call": "Video call on this site",
|
|
8
|
+
"forge.account_dashboard.booking_join_video": "Join video call",
|
|
9
|
+
"forge.account_dashboard.booking_video_opens_at": "Video call: the room opens {datetime}",
|
|
10
|
+
"forge.account_dashboard.booking_video_closed": "Video call: this session's room has closed",
|
|
8
11
|
"forge.account_dashboard.booking_where_label": "Where:",
|
|
9
12
|
"forge.account_dashboard.cancel": "Cancel",
|
|
10
13
|
"forge.account_dashboard.cancel_confirm_confirm": "Cancel membership",
|
|
11
14
|
"forge.account_dashboard.cancel_confirm_free": "Your benefits will stop right away.",
|
|
12
15
|
"forge.account_dashboard.cancel_confirm_keep": "Keep membership",
|
|
13
16
|
"forge.account_dashboard.cancel_confirm_paid": "It will not renew, and you keep your benefits until the end of the period you have paid for.",
|
|
14
|
-
"forge.account_dashboard.cancel_confirm_paid_until": "It will not renew, and you keep your benefits until {
|
|
17
|
+
"forge.account_dashboard.cancel_confirm_paid_until": "It will not renew, and you keep your benefits until {date}.",
|
|
15
18
|
"forge.account_dashboard.cancel_confirm_title": "Cancel your membership?",
|
|
16
19
|
"forge.account_dashboard.cancel_deletion": "Cancel Deletion",
|
|
17
20
|
"forge.account_dashboard.cancel_session": "Cancel session",
|
|
@@ -184,6 +187,41 @@
|
|
|
184
187
|
"forge.bundle_confirmation.tickets_emailed": "Tickets emailed to you",
|
|
185
188
|
"forge.bundle_confirmation.title": "You're all set",
|
|
186
189
|
"forge.bundle_confirmation.view_orders": "View your orders",
|
|
190
|
+
"forge.call_stage.status_ended": "The call has ended",
|
|
191
|
+
"forge.call_stage.status_connected": "Connected",
|
|
192
|
+
"forge.call_stage.status_connecting": "Connecting to the call",
|
|
193
|
+
"forge.call_stage.status_left": "You have left the call",
|
|
194
|
+
"forge.call_stage.status_refused": "Could not join this call",
|
|
195
|
+
"forge.call_stage.status_moving": "Moving you to another server",
|
|
196
|
+
"forge.call_stage.status_moving_detail": "This takes a few seconds and nobody else is dropped.",
|
|
197
|
+
"forge.call_stage.status_move_failed": "Could not move you to another server",
|
|
198
|
+
"forge.call_stage.status_move_failed_detail": "Nobody else was dropped. Try again to rejoin.",
|
|
199
|
+
"forge.call_stage.status_reconnecting": "Connection lost, reconnecting",
|
|
200
|
+
"forge.call_stage.status_reconnecting_detail": "Everyone else is still in the call.",
|
|
201
|
+
"forge.call_stage.status_lost": "Connection lost",
|
|
202
|
+
"forge.call_stage.status_lost_detail": "Everyone else is still in the call. Try again to rejoin.",
|
|
203
|
+
"forge.call_stage.retry": "Try again",
|
|
204
|
+
"forge.call_stage.recording": "This call is being recorded",
|
|
205
|
+
"forge.call_stage.head_count": "In this call: {count}",
|
|
206
|
+
"forge.call_stage.help_hear_not_see_label": "Why can I hear someone but not see them?",
|
|
207
|
+
"forge.call_stage.help_hear_not_see": "A tile with a name and no picture means that person has their camera off, or the call is busy enough that only the people currently speaking are sent in full. Both are normal, and their audio is unaffected.",
|
|
208
|
+
"forge.call_stage.listening": "(listening)",
|
|
209
|
+
"forge.call_stage.you": "You",
|
|
210
|
+
"forge.call_stage.you_not_connected": "You (not connected yet)",
|
|
211
|
+
"forge.call_stage.mute": "Mute",
|
|
212
|
+
"forge.call_stage.unmute": "Unmute",
|
|
213
|
+
"forge.call_stage.camera_start": "Start camera",
|
|
214
|
+
"forge.call_stage.camera_stop": "Stop camera",
|
|
215
|
+
"forge.call_stage.help_camera_stop_label": "What does Stop camera do?",
|
|
216
|
+
"forge.call_stage.help_camera_stop": "It stops sending your picture: the others see your name instead. The browser keeps the camera open so that starting it again is instant and does not ask for permission a second time, which means its indicator light can stay on until you leave the call.",
|
|
217
|
+
"forge.call_stage.screen_start": "Share screen",
|
|
218
|
+
"forge.call_stage.screen_stop": "Stop sharing",
|
|
219
|
+
"forge.call_stage.help_screen_label": "What does sharing my screen send?",
|
|
220
|
+
"forge.call_stage.help_screen": "Whatever you pick in the browser prompt, live, to everyone in the call. Your camera keeps running alongside it rather than being replaced, so people can still see you while you present.",
|
|
221
|
+
"forge.call_stage.leave": "Leave",
|
|
222
|
+
"forge.call_stage.lost_microphone": "Your microphone was turned off when the connection dropped. Unmute to be heard again.",
|
|
223
|
+
"forge.call_stage.lost_camera": "Your camera was turned off when the connection dropped. Start it again to be seen.",
|
|
224
|
+
"forge.call_stage.lost_screen": "Your screen share ended when the connection dropped. Share it again to continue.",
|
|
187
225
|
"forge.cancellation_terms.title": "Cancellation policy",
|
|
188
226
|
"forge.cart.aria_close": "Close cart",
|
|
189
227
|
"forge.cart.aria_open": "Open cart",
|
|
@@ -790,7 +828,7 @@
|
|
|
790
828
|
"forge.membership_checkout.payment_title": "Payment",
|
|
791
829
|
"forge.membership_checkout.per_month": "per month",
|
|
792
830
|
"forge.membership_checkout.per_year": "per year",
|
|
793
|
-
"forge.membership_checkout.pwyw_charged_as": "You will be charged {
|
|
831
|
+
"forge.membership_checkout.pwyw_charged_as": "You will be charged {amount}.",
|
|
794
832
|
"forge.membership_checkout.pwyw_label": "How much would you like to pay?",
|
|
795
833
|
"forge.membership_checkout.pwyw_min": "(minimum {amount})",
|
|
796
834
|
"forge.membership_checkout.select_tier": "Select this tier",
|
package/src/ui/index.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
// Forge UI — Tier-2 styled blocks (theme-aware, opinionated) + the theme
|
|
2
2
|
// contract. Built on the Tier-1 headless primitives in `forge/ui/headless`.
|
|
3
|
+
|
|
4
|
+
// Compiled Tailwind utilities for every className in Forge's source, generated
|
|
5
|
+
// by `npm run build:css` (see tailwind.input.css for why the consumer site's
|
|
6
|
+
// own Tailwind scan cannot be relied on). Bundlers deliver a side-effect CSS
|
|
7
|
+
// import from a library the same way the rich-text.css imports already ship.
|
|
8
|
+
// Compiled Tailwind utilities for every className in Forge's source, generated
|
|
9
|
+
// by `npm run build:css` (see tailwind.input.css for why the consumer site's
|
|
10
|
+
// own Tailwind scan cannot be relied on). Bundlers deliver a side-effect CSS
|
|
11
|
+
// import from a library the same way the rich-text.css imports already ship.
|
|
12
|
+
//
|
|
13
|
+
// This import MUST stay a side effect, and the reason is not stylistic. A site
|
|
14
|
+
// deployed before this shipped has a `styles.css` we never rewrite: Update
|
|
15
|
+
// Theme repins Forge's version and touches nothing else. So an explicit
|
|
16
|
+
// `@import` in the starter reaches NEW sites only, and every existing site
|
|
17
|
+
// would silently lose its Forge layouts the moment it repinned. Delivering
|
|
18
|
+
// from the package is the only route that reaches a site nobody edits.
|
|
19
|
+
//
|
|
20
|
+
// The cost is that it reaches every consumer, including our own apps, and the
|
|
21
|
+
// file is not shy: `@layer theme` redefines 23 root design tokens and
|
|
22
|
+
// `@layer properties` resets 43 `--tw-*` on the universal selector. That made
|
|
23
|
+
// the admin dashboard sidebar disappear (2026-08-22). Admin therefore aliases
|
|
24
|
+
// this file to an empty stub in its vite config and `@source`s Forge's source
|
|
25
|
+
// instead. A monorepo app that renders Forge components should do the same.
|
|
26
|
+
import "./styled/forge-utilities.css";
|
|
27
|
+
|
|
3
28
|
export {
|
|
4
29
|
ForgeThemeProvider,
|
|
5
30
|
useForgeTheme,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { CallStage } from "./CallStage";
|
|
4
|
+
import { BookingCallProvider } from "./bookingSession";
|
|
5
|
+
|
|
6
|
+
export type BookingCallScreenProps = {
|
|
7
|
+
bookingId: string;
|
|
8
|
+
/** Shown as the call's title: the product the session was bought on. */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Leave. The caller unmounts this subtree, which is what ends the call. */
|
|
11
|
+
onLeave: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One booking's call: the provider wired to the credential endpoint, and the
|
|
16
|
+
* one call UI inside it.
|
|
17
|
+
*
|
|
18
|
+
* A DEFAULT export on purpose. `AccountDashboard` (in the `./ui` entry every
|
|
19
|
+
* site loads) reaches this through `React.lazy(() => import(...))`, so the
|
|
20
|
+
* media SDK and mediasoup are fetched by the browser only when somebody
|
|
21
|
+
* actually presses Join, rather than by every visitor to every account page.
|
|
22
|
+
* The provider connects on mount and closes on unmount, so the caller's
|
|
23
|
+
* "joined" state is exactly the call's lifetime: leaving goes through state,
|
|
24
|
+
* never through a `close()` that leaves a live provider mounted to reconnect
|
|
25
|
+
* on its next render.
|
|
26
|
+
*/
|
|
27
|
+
export default function BookingCallScreen({ bookingId, title, onLeave }: BookingCallScreenProps): ReactNode {
|
|
28
|
+
return (
|
|
29
|
+
<BookingCallProvider bookingId={bookingId}>
|
|
30
|
+
<CallStage title={title} onLeave={onLeave} />
|
|
31
|
+
</BookingCallProvider>
|
|
32
|
+
);
|
|
33
|
+
}
|