@ossclip/scenes 0.1.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.
@@ -0,0 +1,86 @@
1
+ import React from "react";
2
+ import { z } from "zod/v4";
3
+ import { RuleCardProps, type Theme } from "@ossclip/core/browser";
4
+ import { pop, rise, useEnter } from "../anim";
5
+ import { editStyle, type ElementEdits } from "../editable";
6
+
7
+ export const RuleCard: React.FC<{
8
+ props: z.infer<typeof RuleCardProps>;
9
+ theme: Theme;
10
+ edits?: ElementEdits;
11
+ }> = ({ props, theme, edits }) => {
12
+ const p0 = useEnter(0);
13
+ const p1 = useEnter(8);
14
+ return (
15
+ <div
16
+ style={{
17
+ display: "flex",
18
+ flexDirection: "column",
19
+ alignItems: "center",
20
+ gap: 26,
21
+ fontFamily: theme.fontDisplay,
22
+ width: "100%",
23
+ padding: "0 30px",
24
+ }}
25
+ >
26
+ <div
27
+ style={{
28
+ ...pop(p0),
29
+ width: "100%",
30
+ background: theme.fg,
31
+ color: theme.bg,
32
+ borderRadius: theme.radiusPx,
33
+ padding: "40px 48px",
34
+ textAlign: "left",
35
+ }}
36
+ >
37
+ <div
38
+ data-edit-id="kicker"
39
+ style={{
40
+ fontSize: 30,
41
+ fontWeight: 800,
42
+ letterSpacing: "0.28em",
43
+ textTransform: "uppercase",
44
+ color: "#55555E",
45
+ marginBottom: 14,
46
+ fontFamily: theme.fontMono,
47
+ ...editStyle(edits, "kicker"),
48
+ }}
49
+ >
50
+ {props.kicker}
51
+ </div>
52
+ <div
53
+ data-edit-id="text"
54
+ style={{
55
+ fontSize: 72,
56
+ fontWeight: 900,
57
+ lineHeight: 1.02,
58
+ textTransform: "uppercase",
59
+ ...editStyle(edits, "text"),
60
+ }}
61
+ >
62
+ {props.text}
63
+ </div>
64
+ </div>
65
+ {props.struck ? (
66
+ <div
67
+ data-edit-id="struck"
68
+ style={{
69
+ ...rise(p1),
70
+ fontSize: 44,
71
+ fontWeight: 800,
72
+ color: theme.muted,
73
+ textDecoration: "line-through",
74
+ textDecorationThickness: 6,
75
+ textDecorationColor: theme.danger,
76
+ textTransform: "uppercase",
77
+ letterSpacing: "0.04em",
78
+ ...editStyle(edits, "struck"),
79
+ }}
80
+ >
81
+ {props.struck}
82
+ </div>
83
+ ) : null}
84
+ </div>
85
+ );
86
+ };
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import { z } from "zod/v4";
3
+ import { Img, interpolate, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
4
+ import { ScreenshotFrameProps, type Theme } from "@ossclip/core/browser";
5
+ import { pop, useEnter } from "../anim";
6
+ import { editStyle, type ElementEdits } from "../editable";
7
+
8
+ export const ScreenshotFrame: React.FC<{
9
+ props: z.infer<typeof ScreenshotFrameProps>;
10
+ theme: Theme;
11
+ edits?: ElementEdits;
12
+ }> = ({ props, theme, edits }) => {
13
+ const p = useEnter(0);
14
+ const labelP = useEnter(8);
15
+ const frame = useCurrentFrame();
16
+ const { fps } = useVideoConfig();
17
+ const drift = props.kenBurns
18
+ ? interpolate(frame, [0, fps * 8], [1.0, 1.08], { extrapolateRight: "clamp" })
19
+ : 1;
20
+ const src = props.src
21
+ ? /^https?:\/\//.test(props.src)
22
+ ? props.src
23
+ : staticFile(props.src)
24
+ : null;
25
+ return (
26
+ <div
27
+ data-edit-id="image"
28
+ style={{ ...pop(p), position: "relative", width: "94%", ...editStyle(edits, "image") }}
29
+ >
30
+ <div
31
+ style={{
32
+ overflow: "hidden",
33
+ borderRadius: theme.radiusPx / 2,
34
+ border: `2px solid ${theme.cardBorder}`,
35
+ background: theme.cardBg,
36
+ boxShadow: "0 24px 80px rgba(0,0,0,0.5)",
37
+ }}
38
+ >
39
+ {src ? (
40
+ <Img
41
+ src={src}
42
+ style={{ width: "100%", display: "block", transform: `scale(${drift})` }}
43
+ />
44
+ ) : (
45
+ <div
46
+ style={{
47
+ height: 420,
48
+ display: "flex",
49
+ flexDirection: "column",
50
+ gap: 14,
51
+ padding: 30,
52
+ transform: `scale(${drift})`,
53
+ }}
54
+ >
55
+ {[0.9, 0.75, 0.85, 0.6, 0.8, 0.5].map((w, i) => (
56
+ <div
57
+ key={i}
58
+ style={{
59
+ height: 22,
60
+ width: `${w * 100}%`,
61
+ borderRadius: 6,
62
+ background: i % 3 === 0 ? theme.cardBorder : "#22222B",
63
+ }}
64
+ />
65
+ ))}
66
+ </div>
67
+ )}
68
+ </div>
69
+ {props.label ? (
70
+ <div
71
+ data-edit-id="label"
72
+ style={{
73
+ ...pop(labelP),
74
+ position: "absolute",
75
+ right: -8,
76
+ bottom: -20,
77
+ background: theme.fg,
78
+ color: theme.bg,
79
+ fontFamily: theme.fontMono,
80
+ fontSize: 28,
81
+ fontWeight: 800,
82
+ letterSpacing: "0.08em",
83
+ padding: "12px 22px",
84
+ borderRadius: 10,
85
+ textTransform: "uppercase",
86
+ ...editStyle(edits, "label"),
87
+ }}
88
+ >
89
+ {props.label}
90
+ </div>
91
+ ) : null}
92
+ </div>
93
+ );
94
+ };
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { z } from "zod/v4";
3
+ import { StatCardProps, type Theme } from "@ossclip/core/browser";
4
+ import { pop, rise, useEnter } from "../anim";
5
+ import { editStyle, type ElementEdits } from "../editable";
6
+
7
+ export const StatCard: React.FC<{
8
+ props: z.infer<typeof StatCardProps>;
9
+ theme: Theme;
10
+ edits?: ElementEdits;
11
+ }> = ({ props, theme, edits }) => {
12
+ const p0 = useEnter(0);
13
+ const p1 = useEnter(6);
14
+ const inverted = props.inverted;
15
+ return (
16
+ <div
17
+ style={{
18
+ display: "flex",
19
+ flexDirection: "column",
20
+ alignItems: "center",
21
+ gap: 30,
22
+ fontFamily: theme.fontDisplay,
23
+ width: "100%",
24
+ padding: "0 30px",
25
+ }}
26
+ >
27
+ <div
28
+ style={{
29
+ ...pop(p0),
30
+ display: "flex",
31
+ alignItems: "center",
32
+ justifyContent: "space-between",
33
+ gap: 40,
34
+ width: "100%",
35
+ background: inverted ? theme.fg : theme.cardBg,
36
+ color: inverted ? theme.bg : theme.fg,
37
+ border: `2px solid ${inverted ? theme.fg : theme.cardBorder}`,
38
+ borderRadius: theme.radiusPx,
39
+ padding: "44px 52px",
40
+ }}
41
+ >
42
+ <div
43
+ data-edit-id="label"
44
+ style={{
45
+ fontSize: 42,
46
+ fontWeight: 800,
47
+ textTransform: "uppercase",
48
+ letterSpacing: "0.06em",
49
+ lineHeight: 1.15,
50
+ maxWidth: "55%",
51
+ ...editStyle(edits, "label"),
52
+ }}
53
+ >
54
+ {props.label}
55
+ </div>
56
+ <div
57
+ data-edit-id="value"
58
+ style={{
59
+ fontSize: 110,
60
+ fontWeight: 900,
61
+ whiteSpace: "nowrap",
62
+ ...editStyle(edits, "value"),
63
+ }}
64
+ >
65
+ {props.value}
66
+ </div>
67
+ </div>
68
+ {props.caption ? (
69
+ <div
70
+ data-edit-id="caption"
71
+ style={{
72
+ ...rise(p1),
73
+ fontSize: 38,
74
+ fontWeight: 800,
75
+ color: theme.fg,
76
+ background: theme.cardBg,
77
+ border: `2px solid ${theme.cardBorder}`,
78
+ borderRadius: theme.radiusPx / 2,
79
+ padding: "16px 30px",
80
+ textTransform: "uppercase",
81
+ letterSpacing: "0.08em",
82
+ ...editStyle(edits, "caption"),
83
+ }}
84
+ >
85
+ {props.caption}
86
+ </div>
87
+ ) : null}
88
+ </div>
89
+ );
90
+ };
@@ -0,0 +1,130 @@
1
+ import React from "react";
2
+ import { z } from "zod/v4";
3
+ import { StrikethroughRevealProps, type Theme } from "@ossclip/core/browser";
4
+ import { rise, useEnter } from "../anim";
5
+ import { revealMetrics, revealRows } from "../fit";
6
+ import { editStyle, type ElementEdits } from "../editable";
7
+
8
+ /**
9
+ * One RENDERED row, with its own strike rule.
10
+ *
11
+ * The rule must be per rendered row, not per logical line: when a line wrapped,
12
+ * the absolutely-positioned bar sat at 50% of the whole two-line block and so
13
+ * struck the gap between the rows rather than either of them (FINDINGS §27).
14
+ *
15
+ * `editId` is keyed by the LOGICAL line (props.lines index), not the rendered
16
+ * row: a wrapped line produces multiple rows here, and all of them share the
17
+ * same id/edits so a user's nudge moves every row of that line together
18
+ * rather than only the first fragment.
19
+ */
20
+ const Row: React.FC<{
21
+ text: string;
22
+ struck: boolean;
23
+ /** Verdict glyph (R16 §66) — ✗/✓ leading the row, none for plain lines. */
24
+ mark: "none" | "cross" | "check";
25
+ delay: number;
26
+ fontSize: number;
27
+ theme: Theme;
28
+ editId: string;
29
+ edits?: ElementEdits;
30
+ }> = ({ text, struck, mark, delay, fontSize, theme, editId, edits }) => {
31
+ const p = useEnter(delay);
32
+ const strike = useEnter(delay + 8);
33
+ return (
34
+ <div
35
+ data-edit-id={editId}
36
+ style={{ ...rise(p, 30), position: "relative", display: "inline-block", ...editStyle(edits, editId) }}
37
+ >
38
+ <span
39
+ style={{
40
+ fontSize,
41
+ fontWeight: 900,
42
+ textTransform: "uppercase",
43
+ lineHeight: 1.08,
44
+ whiteSpace: "nowrap",
45
+ color: struck ? theme.muted : theme.fg,
46
+ }}
47
+ >
48
+ {mark !== "none" ? (
49
+ <span
50
+ style={{
51
+ color: mark === "cross" ? theme.danger : theme.success,
52
+ marginRight: "0.3em",
53
+ }}
54
+ >
55
+ {mark === "cross" ? "✗" : "✓"}
56
+ </span>
57
+ ) : null}
58
+ {text}
59
+ </span>
60
+ {struck ? (
61
+ <div
62
+ style={{
63
+ position: "absolute",
64
+ left: "-2%",
65
+ top: "50%",
66
+ height: fontSize * 0.11,
67
+ width: `${strike * 104}%`,
68
+ background: theme.danger,
69
+ borderRadius: fontSize * 0.055,
70
+ transform: "translateY(-50%) rotate(-2deg)",
71
+ }}
72
+ />
73
+ ) : null}
74
+ </div>
75
+ );
76
+ };
77
+
78
+ export const StrikethroughReveal: React.FC<{
79
+ props: z.infer<typeof StrikethroughRevealProps>;
80
+ theme: Theme;
81
+ widthPx?: number;
82
+ heightPx?: number;
83
+ edits?: ElementEdits;
84
+ }> = ({ props, theme, widthPx, heightPx, edits }) => {
85
+ // The verdict glyph rides in the width estimate ("✗ " ≈ its real footprint)
86
+ // so a marked line shrinks with its glyph instead of overflowing. `mark` is
87
+ // read defensively: baked render-props from before §66 carry no key.
88
+ const texts = props.lines.map((l) =>
89
+ (l.mark ?? "none") !== "none" ? `✗ ${l.text}` : l.text,
90
+ );
91
+ const fontSize = revealMetrics(texts, widthPx, heightPx);
92
+ // Each logical line becomes one or more rows; a line only breaks at an
93
+ // arrow, and the arrow leads the row it points into. lineIndex is kept
94
+ // alongside so every row can carry its logical line's data-edit-id. The
95
+ // glyph marks only the FIRST row of a wrapped line — one verdict per line.
96
+ const rows = props.lines.flatMap((line, lineIndex) =>
97
+ revealRows(line.text, fontSize, widthPx ?? 831).map((text, rowIdx) => ({
98
+ text,
99
+ struck: line.struck,
100
+ mark: rowIdx === 0 ? (line.mark ?? "none") : ("none" as const),
101
+ lineIndex,
102
+ })),
103
+ );
104
+ return (
105
+ <div
106
+ style={{
107
+ display: "flex",
108
+ flexDirection: "column",
109
+ alignItems: "center",
110
+ gap: fontSize * 0.2,
111
+ textAlign: "center",
112
+ fontFamily: theme.fontDisplay,
113
+ }}
114
+ >
115
+ {rows.map((row, i) => (
116
+ <Row
117
+ key={i}
118
+ text={row.text}
119
+ struck={row.struck}
120
+ mark={row.mark}
121
+ delay={i * 6}
122
+ fontSize={fontSize}
123
+ theme={theme}
124
+ editId={`line-${row.lineIndex}`}
125
+ edits={edits}
126
+ />
127
+ ))}
128
+ </div>
129
+ );
130
+ };
@@ -0,0 +1,105 @@
1
+ import React from "react";
2
+ import { z } from "zod/v4";
3
+ import { TerminalMockProps, type Theme } from "@ossclip/core/browser";
4
+ import { pop, rise, useEnter } from "../anim";
5
+ import { editStyle, type ElementEdits } from "../editable";
6
+
7
+ const Window: React.FC<{
8
+ title: string;
9
+ lines: string[];
10
+ delay: number;
11
+ theme: Theme;
12
+ editId: string;
13
+ edits?: ElementEdits;
14
+ }> = ({ title, lines, delay, theme, editId, edits }) => {
15
+ const p = useEnter(delay);
16
+ return (
17
+ <div
18
+ data-edit-id={editId}
19
+ style={{
20
+ ...pop(p),
21
+ background: theme.cardBg,
22
+ border: `2px solid ${theme.cardBorder}`,
23
+ borderRadius: theme.radiusPx / 2,
24
+ overflow: "hidden",
25
+ width: "100%",
26
+ fontFamily: theme.fontMono,
27
+ ...editStyle(edits, editId),
28
+ }}
29
+ >
30
+ <div
31
+ style={{
32
+ display: "flex",
33
+ alignItems: "center",
34
+ gap: 10,
35
+ padding: "14px 20px",
36
+ borderBottom: `2px solid ${theme.cardBorder}`,
37
+ }}
38
+ >
39
+ {["#FF5F57", "#FEBC2E", "#28C840"].map((c) => (
40
+ <div key={c} style={{ width: 16, height: 16, borderRadius: 8, background: c }} />
41
+ ))}
42
+ <div style={{ marginLeft: 10, fontSize: 24, color: theme.muted }}>{title}</div>
43
+ </div>
44
+ <div style={{ padding: "18px 22px", display: "flex", flexDirection: "column", gap: 8 }}>
45
+ {lines.map((line, i) => (
46
+ <div key={i} style={{ fontSize: 27, color: theme.fg, whiteSpace: "pre" }}>
47
+ {line}
48
+ </div>
49
+ ))}
50
+ </div>
51
+ </div>
52
+ );
53
+ };
54
+
55
+ export const TerminalMock: React.FC<{
56
+ props: z.infer<typeof TerminalMockProps>;
57
+ theme: Theme;
58
+ edits?: ElementEdits;
59
+ }> = ({ props, theme, edits }) => {
60
+ const tail = useEnter(props.windows.length * 6 + 4);
61
+ return (
62
+ <div
63
+ style={{
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ alignItems: "center",
67
+ gap: 22,
68
+ width: "100%",
69
+ padding: "0 30px",
70
+ }}
71
+ >
72
+ {props.windows.map((w, i) => (
73
+ <Window
74
+ key={i}
75
+ title={w.title}
76
+ lines={w.lines}
77
+ delay={i * 6}
78
+ theme={theme}
79
+ editId={`window-${i}`}
80
+ edits={edits}
81
+ />
82
+ ))}
83
+ {props.fanOut ? (
84
+ <div
85
+ // The last un-transformable element on any component (R17 §85
86
+ // audit): with an id it joins the generic drag/resize/retype
87
+ // machinery like everything else.
88
+ data-edit-id="fanOut"
89
+ style={{
90
+ ...rise(tail),
91
+ fontFamily: theme.fontDisplay,
92
+ fontSize: 40,
93
+ fontWeight: 900,
94
+ letterSpacing: "0.2em",
95
+ color: theme.fg,
96
+ textTransform: "uppercase",
97
+ ...editStyle(edits, "fanOut"),
98
+ }}
99
+ >
100
+ ⌄ {props.fanOut}
101
+ </div>
102
+ ) : null}
103
+ </div>
104
+ );
105
+ };
@@ -0,0 +1,95 @@
1
+ import React from "react";
2
+ import { z } from "zod/v4";
3
+ import { TitleCardProps, type Theme } from "@ossclip/core/browser";
4
+ import { rise, useEnter } from "../anim";
5
+ import { editStyle, type ElementEdits } from "../editable";
6
+
7
+ export const TitleCard: React.FC<{
8
+ props: z.infer<typeof TitleCardProps>;
9
+ theme: Theme;
10
+ edits?: ElementEdits;
11
+ }> = ({ props, theme, edits }) => {
12
+ const p0 = useEnter(0);
13
+ const p1 = useEnter(5);
14
+ const p2 = useEnter(10);
15
+ // The producer sometimes emits the same string for both fields; the huge
16
+ // emphasis wins and the redundant title is skipped (FINDINGS §5).
17
+ const emphasis = props.emphasis?.trim();
18
+ const titleIsRedundant =
19
+ !!emphasis && emphasis.toLowerCase().includes(props.title.trim().toLowerCase());
20
+ return (
21
+ <div
22
+ style={{
23
+ display: "flex",
24
+ flexDirection: "column",
25
+ alignItems: "center",
26
+ gap: 28,
27
+ textAlign: "center",
28
+ fontFamily: theme.fontDisplay,
29
+ color: theme.fg,
30
+ padding: "0 40px",
31
+ }}
32
+ >
33
+ {props.eyebrow ? (
34
+ <div
35
+ data-edit-id="eyebrow"
36
+ style={{
37
+ ...rise(p0),
38
+ fontSize: 34,
39
+ fontWeight: 700,
40
+ letterSpacing: "0.35em",
41
+ color: theme.muted,
42
+ textTransform: "uppercase",
43
+ ...editStyle(edits, "eyebrow"),
44
+ }}
45
+ >
46
+ {props.eyebrow}
47
+ </div>
48
+ ) : null}
49
+ {emphasis ? (
50
+ <div
51
+ data-edit-id="emphasis"
52
+ style={{
53
+ ...rise(p1, 40),
54
+ fontSize: 210,
55
+ fontWeight: 900,
56
+ lineHeight: 0.95,
57
+ ...editStyle(edits, "emphasis"),
58
+ }}
59
+ >
60
+ {emphasis}
61
+ </div>
62
+ ) : null}
63
+ {!titleIsRedundant ? (
64
+ <div
65
+ data-edit-id="title"
66
+ style={{
67
+ ...rise(emphasis ? p2 : p1, 34),
68
+ fontSize: emphasis ? 64 : 96,
69
+ fontWeight: 900,
70
+ lineHeight: 1.05,
71
+ textTransform: "uppercase",
72
+ letterSpacing: "0.02em",
73
+ ...editStyle(edits, "title"),
74
+ }}
75
+ >
76
+ {props.title}
77
+ </div>
78
+ ) : null}
79
+ {props.sub ? (
80
+ <div
81
+ data-edit-id="sub"
82
+ style={{
83
+ ...rise(p2),
84
+ fontSize: 40,
85
+ fontWeight: 600,
86
+ color: theme.muted,
87
+ ...editStyle(edits, "sub"),
88
+ }}
89
+ >
90
+ {props.sub}
91
+ </div>
92
+ ) : null}
93
+ </div>
94
+ );
95
+ };