@tribe-nest/forge 3.50.0 → 3.53.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 +2 -2
- package/src/data/queries/useCohortEnrolment.ts +87 -0
- package/src/data/queries/useMyCourses.ts +42 -0
- package/src/data/queries/useWebsiteAgent.ts +2 -0
- package/src/i18n/de.json +52 -1
- package/src/i18n/en.json +52 -1
- package/src/index.ts +2 -0
- package/src/types/models.ts +20 -0
- package/src/ui/headless/agent/_tests/pathExclusion.spec.ts +52 -0
- package/src/ui/headless/agent/_tests/useCurrentPath.spec.tsx +54 -0
- package/src/ui/headless/agent/pathExclusion.ts +53 -0
- package/src/ui/headless/agent/useAiAgent.ts +10 -1
- package/src/ui/headless/agent/useCurrentPath.ts +49 -0
- package/src/ui/headless/cohort/useCohortCheckout.ts +180 -0
- package/src/ui/index.ts +7 -0
- package/src/ui/styled/AccountDashboard.tsx +102 -1
- package/src/ui/styled/AiAgentWidget.tsx +3 -2
- package/src/ui/styled/BlogPost.tsx +1 -1
- package/src/ui/styled/ChatRoom.tsx +2 -2
- package/src/ui/styled/Checkout.tsx +125 -125
- package/src/ui/styled/CoachingBooking.tsx +1 -1
- package/src/ui/styled/CoachingDetail.tsx +4 -4
- package/src/ui/styled/CohortCheckout.tsx +450 -0
- package/src/ui/styled/CohortConfirmation.tsx +259 -0
- package/src/ui/styled/CohortPage.tsx +16 -0
- package/src/ui/styled/CourseDetail.tsx +4 -4
- package/src/ui/styled/EventDetail.tsx +4 -4
- package/src/ui/styled/PaystackPayButton.tsx +1 -1
- package/src/ui/styled/PodcastEpisode.tsx +1 -1
- package/src/ui/styled/ProductDetail.tsx +4 -4
- package/src/ui/styled/_tests/CohortCheckout.spec.tsx +232 -0
- package/src/ui/styled/_tests/packagedCssIsCurrent.spec.ts +43 -0
- package/src/ui/styled/_tests/utilityPrefix.spec.ts +156 -0
- package/src/ui/styled/broadcast/BroadcastMoqStage.tsx +1 -1
- package/src/ui/styled/chat/ChatThreadPanel.tsx +1 -1
- package/src/ui/styled/forge-utilities.css +463 -731
- package/src/ui/styled/members/MemberCallsCalendar.tsx +2 -2
- package/src/ui/styled/members/MemberHomeLayout.tsx +4 -4
- package/src/ui/styled/members/MemberPostsFeed.tsx +1 -1
- package/src/ui/styled/members/PostCollectionsGrid.tsx +1 -1
- package/src/ui/styled/musicLinkStyles.tsx +4 -4
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every Tailwind utility in a Forge component carries the `fg:` prefix.
|
|
7
|
+
*
|
|
8
|
+
* THE ONE RULE, and the only one. Forge builds its stylesheet with
|
|
9
|
+
* `prefix(fg)` (see tailwind.input.css), so `fg:flex` compiles to `.fg\:flex`
|
|
10
|
+
* and a bare `flex` compiles to NOTHING. An unprefixed class is therefore not a
|
|
11
|
+
* style that loses an argument, it is a style that does not exist: the element
|
|
12
|
+
* renders unstyled and no error is raised anywhere.
|
|
13
|
+
*
|
|
14
|
+
* WHY THE PREFIX EXISTS. Tailwind utilities are global and unprefixed by
|
|
15
|
+
* construction, so Forge's `.hidden` and a host's `.hidden` are the same rule.
|
|
16
|
+
* Shipping Forge's copies bare meant one of the two had to lose everywhere, and
|
|
17
|
+
* both orderings shipped a bug: Forge above the host collapsed the admin
|
|
18
|
+
* sidebar (027b8aa9d), Forge below the host pinned a coaching booking page's
|
|
19
|
+
* "Select availability" card over its cover image on every published site, and
|
|
20
|
+
* made `hidden sm:block` blocks permanently invisible. A cascade layer picks
|
|
21
|
+
* one global winner for a name that means different things on different
|
|
22
|
+
* elements, so it could never settle it. The prefix removes the collision.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IS NOT A UTILITY, and must stay bare:
|
|
25
|
+
* - the hand-written classes in Forge's own stylesheets (`cf-rise`,
|
|
26
|
+
* `forge-rich`, `forge-spin`, …), which are real CSS rules;
|
|
27
|
+
* - `group` and `peer`, which are Tailwind MARKERS targeted by `group-*` /
|
|
28
|
+
* `peer-*` and never emitted as rules of their own;
|
|
29
|
+
* - anything arriving through the `className` PROP, which belongs to the host.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** Hand-written CSS in Forge's own stylesheets, plus Tailwind's markers. */
|
|
33
|
+
const NOT_UTILITIES = new Set([
|
|
34
|
+
"cf-card",
|
|
35
|
+
"cf-halo",
|
|
36
|
+
"cf-pop",
|
|
37
|
+
"cf-ring",
|
|
38
|
+
"cf-rise",
|
|
39
|
+
"cf-tick",
|
|
40
|
+
"forge-community-composer-editable",
|
|
41
|
+
"forge-pa-compact",
|
|
42
|
+
"forge-rich",
|
|
43
|
+
"forge-spin",
|
|
44
|
+
"group",
|
|
45
|
+
"peer",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const PREFIX = "fg:";
|
|
49
|
+
|
|
50
|
+
function walk(dir: string, out: string[] = []): string[] {
|
|
51
|
+
for (const entry of readdirSync(dir)) {
|
|
52
|
+
const full = join(dir, entry);
|
|
53
|
+
if (statSync(full).isDirectory()) {
|
|
54
|
+
if (entry !== "_tests") walk(full, out);
|
|
55
|
+
} else if (full.endsWith(".tsx") || full.endsWith(".ts")) out.push(full);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The whole `className=` value, braces and backticks balanced.
|
|
62
|
+
*
|
|
63
|
+
* A regex cannot do this, and the naive one nearly shipped the bug it is meant
|
|
64
|
+
* to prevent: `` className={`pb-32 md:pb-0${className ? ` ${className}` : ""}`} ``
|
|
65
|
+
* contains a NESTED backtick, so a `` /\{`([^`]*)`\}/ `` pattern never matched it
|
|
66
|
+
* and four components sat unprefixed while the guard reported clean.
|
|
67
|
+
*/
|
|
68
|
+
function classNameExpressions(src: string): Array<{ expr: string; index: number }> {
|
|
69
|
+
const found: Array<{ expr: string; index: number }> = [];
|
|
70
|
+
const marker = "className=";
|
|
71
|
+
for (let i = src.indexOf(marker); i !== -1; i = src.indexOf(marker, i + 1)) {
|
|
72
|
+
let j = i + marker.length;
|
|
73
|
+
if (src[j] === '"') {
|
|
74
|
+
const end = src.indexOf('"', j + 1);
|
|
75
|
+
if (end === -1) continue;
|
|
76
|
+
found.push({ expr: src.slice(j, end + 1), index: i });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (src[j] !== "{") continue;
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let inTemplate = false;
|
|
82
|
+
for (; j < src.length; j += 1) {
|
|
83
|
+
const ch = src[j];
|
|
84
|
+
if (ch === "\\") { j += 1; continue; }
|
|
85
|
+
if (ch === "`") inTemplate = !inTemplate;
|
|
86
|
+
else if (ch === "{") depth += 1;
|
|
87
|
+
else if (ch === "}") {
|
|
88
|
+
depth -= 1;
|
|
89
|
+
if (depth === 0) break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
found.push({ expr: src.slice(i + marker.length, j + 1), index: i });
|
|
93
|
+
}
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every literal class token in a className expression, interpolations aside. */
|
|
98
|
+
function classTokens(expr: string): string[] {
|
|
99
|
+
const tokens: string[] = [];
|
|
100
|
+
|
|
101
|
+
// 1. Quoted strings, which is where a ternary keeps its classes:
|
|
102
|
+
// `${open ? "fg:flex" : "fg:hidden"}`.
|
|
103
|
+
for (const m of expr.matchAll(/"([^"]*)"|'([^']*)'/g)) {
|
|
104
|
+
tokens.push(...(m[1] ?? m[2] ?? "").split(/\s+/));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Template TEXT, with every `${...}` removed first so an interpolation's
|
|
108
|
+
// own code can never be mistaken for a class name.
|
|
109
|
+
let stripped = "";
|
|
110
|
+
let depth = 0;
|
|
111
|
+
for (let i = 0; i < expr.length; i += 1) {
|
|
112
|
+
if (expr[i] === "$" && expr[i + 1] === "{") { depth += 1; i += 1; continue; }
|
|
113
|
+
if (depth > 0) {
|
|
114
|
+
if (expr[i] === "{") depth += 1;
|
|
115
|
+
else if (expr[i] === "}") depth -= 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
stripped += expr[i];
|
|
119
|
+
}
|
|
120
|
+
for (const m of stripped.matchAll(/`([^`]*)`/g)) tokens.push(...(m[1] ?? "").split(/\s+/));
|
|
121
|
+
|
|
122
|
+
return tokens.filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("Forge utilities are prefixed", () => {
|
|
126
|
+
it("has no unprefixed utility in any component", () => {
|
|
127
|
+
const root = join(__dirname, "..", "..", "..");
|
|
128
|
+
const offences: string[] = [];
|
|
129
|
+
|
|
130
|
+
for (const file of walk(root)) {
|
|
131
|
+
const src = readFileSync(file, "utf-8");
|
|
132
|
+
for (const { expr, index } of classNameExpressions(src)) {
|
|
133
|
+
for (const cls of classTokens(expr)) {
|
|
134
|
+
if (cls.startsWith(PREFIX) || NOT_UTILITIES.has(cls)) continue;
|
|
135
|
+
const line = src.slice(0, index).split("\n").length;
|
|
136
|
+
offences.push(
|
|
137
|
+
`${file.slice(file.indexOf("packages/"))}:${line} "${cls}" has no fg: prefix, so it compiles to nothing and silently does not apply. Write "fg:${cls}", or add it to NOT_UTILITIES if it is hand-written CSS.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
expect(offences, offences.join("\n")).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("ships no bare utility in the packaged stylesheet", () => {
|
|
147
|
+
// The other half, measured on the OUTPUT rather than the source: even if a
|
|
148
|
+
// component slipped one through, nothing unprefixed may reach a host's page,
|
|
149
|
+
// because that is the rule a bare class would break.
|
|
150
|
+
const css = readFileSync(join(__dirname, "..", "forge-utilities.css"), "utf-8");
|
|
151
|
+
const bare = [...css.matchAll(/^\s*\.([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:,|\{)/gm)]
|
|
152
|
+
.map((m) => m[1]!)
|
|
153
|
+
.filter((c) => !c.startsWith("fg") && !NOT_UTILITIES.has(c));
|
|
154
|
+
expect([...new Set(bare)], "unprefixed rules in the shipped stylesheet").toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -120,5 +120,5 @@ export function BroadcastMoqStage({ url, name, muted, onFailed }: BroadcastMoqSt
|
|
|
120
120
|
};
|
|
121
121
|
}, [url, name, muted]);
|
|
122
122
|
|
|
123
|
-
return <div ref={hostRef} className="h-full w-full bg-black" />;
|
|
123
|
+
return <div ref={hostRef} className="fg:h-full fg:w-full fg:bg-black" />;
|
|
124
124
|
}
|
|
@@ -48,7 +48,7 @@ export function ChatThreadPanel({
|
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<section
|
|
51
|
-
className="fixed inset-0 z-50 md:static md:z-auto md:w-[360px] flex flex-col"
|
|
51
|
+
className="fg:fixed fg:inset-0 fg:z-50 fg:md:static fg:md:z-auto fg:md:w-[360px] fg:flex fg:flex-col"
|
|
52
52
|
style={{ background: theme.colors.background, borderLeft: border }}
|
|
53
53
|
>
|
|
54
54
|
<header style={{ padding: "12px 16px", borderBottom: border, display: "flex", alignItems: "center", gap: 12 }}>
|