@fluid-app/fluid-cli-widget 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,109 @@
1
+ import { widgetPackage } from "../manifest";
2
+
3
+ interface SourceWidgetLike {
4
+ readonly name: string;
5
+ readonly displayName?: string;
6
+ readonly description?: string;
7
+ readonly icon?: string;
8
+ readonly category?: string;
9
+ readonly propertySchema?: Readonly<Record<string, unknown>>;
10
+ readonly defaultProps?: Readonly<Record<string, unknown>>;
11
+ readonly container?: string;
12
+ readonly minSdkVersion?: string;
13
+ readonly resizable?: unknown;
14
+ readonly component: unknown;
15
+ }
16
+
17
+ interface RuntimeWidgetRegistration {
18
+ readonly type: string;
19
+ readonly name: string;
20
+ readonly displayName: string;
21
+ readonly description: string;
22
+ readonly icon: string;
23
+ readonly category: string;
24
+ readonly propertySchema: Readonly<Record<string, unknown>>;
25
+ readonly defaultProps: Readonly<Record<string, unknown>>;
26
+ readonly container: string;
27
+ readonly minSdkVersion: string;
28
+ readonly resizable: boolean | "horizontal" | "vertical" | "both";
29
+ readonly component: unknown;
30
+ }
31
+
32
+ interface LocalFluidWidgetsGlobal {
33
+ registerPackage(registration: {
34
+ readonly packageId: string;
35
+ readonly version: string;
36
+ readonly widgets: readonly RuntimeWidgetRegistration[];
37
+ }): void;
38
+ }
39
+
40
+ const widgets = widgetPackage.widgets.map((widget: SourceWidgetLike) => {
41
+ const type = `${widgetPackage.packageId}.${widget.name}`;
42
+ return {
43
+ type,
44
+ name: widget.name,
45
+ displayName: widget.displayName ?? widget.name,
46
+ description: widget.description ?? `Custom widget ${widget.name}`,
47
+ icon: widget.icon ?? "box",
48
+ category: widget.category ?? "components",
49
+ propertySchema: {
50
+ ...widget.propertySchema,
51
+ widgetType: type,
52
+ },
53
+ defaultProps: widget.defaultProps ?? {},
54
+ container: widget.container ?? "block",
55
+ minSdkVersion: widget.minSdkVersion ?? "0.0.0",
56
+ resizable: normalizeResizable(widget.resizable),
57
+ component: widget.component,
58
+ } satisfies RuntimeWidgetRegistration;
59
+ });
60
+
61
+ const fluidWidgets =
62
+ typeof window === "undefined"
63
+ ? undefined
64
+ : (window as Window & { readonly FluidWidgets?: LocalFluidWidgetsGlobal })
65
+ .FluidWidgets;
66
+
67
+ if (fluidWidgets && typeof fluidWidgets.registerPackage === "function") {
68
+ fluidWidgets.registerPackage({
69
+ packageId: widgetPackage.packageId,
70
+ version: widgetPackage.version,
71
+ widgets,
72
+ });
73
+ } else {
74
+ console.warn("FluidWidgets registry is not installed.");
75
+ }
76
+
77
+ function normalizeResizable(
78
+ resizable: unknown,
79
+ ): RuntimeWidgetRegistration["resizable"] {
80
+ if (
81
+ resizable === true ||
82
+ resizable === "horizontal" ||
83
+ resizable === "vertical" ||
84
+ resizable === "both"
85
+ ) {
86
+ return resizable;
87
+ }
88
+
89
+ if (isRecord(resizable)) {
90
+ const horizontal = resizable.horizontal === true;
91
+ const vertical = resizable.vertical === true;
92
+ if (horizontal && vertical) return "both";
93
+ if (horizontal) return "horizontal";
94
+ if (vertical) return "vertical";
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ function isRecord(value: unknown): value is Record<string, unknown> {
101
+ return typeof value === "object" && value !== null && !Array.isArray(value);
102
+ }
103
+
104
+ export { widgetPackage } from "../manifest";
105
+ export { ReviewCarousel } from "./widgets/review-carousel/ReviewCarousel";
106
+ export type {
107
+ ReviewCarouselProps,
108
+ ReviewCarouselReview,
109
+ } from "./widgets/review-carousel/ReviewCarousel";
@@ -0,0 +1,60 @@
1
+ import { createPreview } from "@fluid-app/portal-sdk/preview";
2
+ import "../styles.css";
3
+ import { widgetPackages } from "../manifest";
4
+
5
+ createPreview({
6
+ widgetPackages: widgetPackages.map((widgetPackage) => ({
7
+ manifestVersion: 1,
8
+ packageId: widgetPackage.packageId,
9
+ packageType: widgetPackage.packageType,
10
+ version: widgetPackage.version,
11
+ remoteEntryUrl: "/__runtime-entry__",
12
+ cssUrls: widgetPackage.cssUrls,
13
+ widgets: widgetPackage.widgets.map((widget) => {
14
+ const type = `${widgetPackage.packageId}.${widget.name}`;
15
+ return {
16
+ type,
17
+ name: widget.name,
18
+ displayName: widget.displayName ?? widget.name,
19
+ description: widget.description ?? `Custom widget ${widget.name}`,
20
+ icon: widget.icon ?? "box",
21
+ category: widget.category ?? "components",
22
+ propertySchema: {
23
+ ...widget.propertySchema,
24
+ widgetType: type,
25
+ },
26
+ defaultProps: widget.defaultProps,
27
+ container: widget.container ?? "block",
28
+ minSdkVersion: widget.minSdkVersion ?? "0.0.0",
29
+ resizable: normalizeResizable(widget.resizable),
30
+ };
31
+ }),
32
+ })),
33
+ });
34
+
35
+ function normalizeResizable(
36
+ resizable: unknown,
37
+ ): boolean | "horizontal" | "vertical" | "both" {
38
+ if (
39
+ resizable === true ||
40
+ resizable === "horizontal" ||
41
+ resizable === "vertical" ||
42
+ resizable === "both"
43
+ ) {
44
+ return resizable;
45
+ }
46
+
47
+ if (isRecord(resizable)) {
48
+ const horizontal = resizable["horizontal"] === true;
49
+ const vertical = resizable["vertical"] === true;
50
+ if (horizontal && vertical) return "both";
51
+ if (horizontal) return "horizontal";
52
+ if (vertical) return "vertical";
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,17 @@
1
+ import { createRoot } from "react-dom/client";
2
+ import "../styles.css";
3
+ import { ReviewCarousel } from "./widgets/review-carousel/ReviewCarousel";
4
+
5
+ const rootElement = document.getElementById("root");
6
+
7
+ if (!rootElement) {
8
+ throw new Error("Missing #root element");
9
+ }
10
+
11
+ createRoot(rootElement).render(
12
+ <main className="fluid-widget-preview-shell">
13
+ <div className="fluid-widget-preview-frame">
14
+ <ReviewCarousel />
15
+ </div>
16
+ </main>,
17
+ );
@@ -0,0 +1,95 @@
1
+ import { useMemo, useState } from "react";
2
+
3
+ export interface ReviewCarouselReview {
4
+ readonly quote: string;
5
+ readonly author: string;
6
+ readonly role: string;
7
+ }
8
+
9
+ export interface ReviewCarouselProps {
10
+ readonly eyebrow?: string;
11
+ readonly title?: string;
12
+ readonly reviews?: readonly ReviewCarouselReview[];
13
+ }
14
+
15
+ const defaultReviews: readonly ReviewCarouselReview[] = [
16
+ {
17
+ quote:
18
+ "Fluid widgets let us ship focused portal experiences without coupling releases to the host application.",
19
+ author: "Maya Chen",
20
+ role: "VP of Customer Experience",
21
+ },
22
+ {
23
+ quote:
24
+ "The package boundary keeps authoring fast while still giving our team a clean publish path.",
25
+ author: "Jordan Ellis",
26
+ role: "Solutions Architect",
27
+ },
28
+ {
29
+ quote:
30
+ "We can preview, validate, and publish a reusable widget package from a small standalone project.",
31
+ author: "Avery Brooks",
32
+ role: "Portal Engineer",
33
+ },
34
+ ];
35
+
36
+ export function ReviewCarousel({
37
+ eyebrow = "Customer stories",
38
+ title = "Reviews that travel with your portal",
39
+ reviews = defaultReviews,
40
+ }: ReviewCarouselProps): React.JSX.Element {
41
+ const safeReviews = reviews.length > 0 ? reviews : defaultReviews;
42
+ const [activeIndex, setActiveIndex] = useState(0);
43
+ const activeReview = safeReviews[activeIndex] ?? safeReviews[0];
44
+
45
+ const countLabel = useMemo(
46
+ () => `${activeIndex + 1} / ${safeReviews.length}`,
47
+ [activeIndex, safeReviews.length],
48
+ );
49
+
50
+ function goToPrevious(): void {
51
+ setActiveIndex((current) =>
52
+ current === 0 ? safeReviews.length - 1 : current - 1,
53
+ );
54
+ }
55
+
56
+ function goToNext(): void {
57
+ setActiveIndex((current) =>
58
+ current === safeReviews.length - 1 ? 0 : current + 1,
59
+ );
60
+ }
61
+
62
+ return (
63
+ <section className="review-carousel" aria-label={title}>
64
+ <p className="review-carousel__eyebrow">{eyebrow}</p>
65
+ <h2 className="review-carousel__title">{title}</h2>
66
+ <blockquote className="review-carousel__quote">
67
+ “{activeReview.quote}”
68
+ </blockquote>
69
+ <div className="review-carousel__footer">
70
+ <div>
71
+ <p className="review-carousel__author">{activeReview.author}</p>
72
+ <p className="review-carousel__role">{activeReview.role}</p>
73
+ </div>
74
+ <div className="review-carousel__controls" aria-label={countLabel}>
75
+ <button
76
+ className="review-carousel__button"
77
+ type="button"
78
+ aria-label="Show previous review"
79
+ onClick={goToPrevious}
80
+ >
81
+
82
+ </button>
83
+ <button
84
+ className="review-carousel__button"
85
+ type="button"
86
+ aria-label="Show next review"
87
+ onClick={goToNext}
88
+ >
89
+
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </section>
94
+ );
95
+ }
@@ -0,0 +1,173 @@
1
+ :root {
2
+ color: #111827;
3
+ background: #f8fafc;
4
+ font-family:
5
+ Inter,
6
+ ui-sans-serif,
7
+ system-ui,
8
+ -apple-system,
9
+ BlinkMacSystemFont,
10
+ "Segoe UI",
11
+ sans-serif;
12
+ }
13
+
14
+ body {
15
+ margin: 0;
16
+ }
17
+
18
+ button {
19
+ font: inherit;
20
+ }
21
+
22
+ .fluid-widget-preview-shell {
23
+ min-height: 100vh;
24
+ padding: 48px 24px;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ .fluid-widget-preview-frame {
29
+ max-width: 960px;
30
+ margin: 0 auto;
31
+ }
32
+
33
+ .review-carousel {
34
+ border: 1px solid #e5e7eb;
35
+ border-radius: 24px;
36
+ padding: 32px;
37
+ background: #ffffff;
38
+ box-shadow: 0 24px 80px rgb(15 23 42 / 10%);
39
+ }
40
+
41
+ .review-carousel__eyebrow {
42
+ margin: 0 0 8px;
43
+ color: #4f46e5;
44
+ font-size: 0.75rem;
45
+ font-weight: 700;
46
+ letter-spacing: 0.08em;
47
+ text-transform: uppercase;
48
+ }
49
+
50
+ .review-carousel__title {
51
+ margin: 0;
52
+ color: #111827;
53
+ font-size: clamp(2rem, 4vw, 3.5rem);
54
+ line-height: 0.95;
55
+ letter-spacing: -0.05em;
56
+ }
57
+
58
+ .review-carousel__quote {
59
+ margin: 32px 0 0;
60
+ color: #1f2937;
61
+ font-size: clamp(1.25rem, 2vw, 1.75rem);
62
+ line-height: 1.35;
63
+ }
64
+
65
+ .review-carousel__footer {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: space-between;
69
+ gap: 24px;
70
+ margin-top: 32px;
71
+ }
72
+
73
+ .review-carousel__author {
74
+ margin: 0;
75
+ color: #111827;
76
+ font-weight: 700;
77
+ }
78
+
79
+ .review-carousel__role {
80
+ margin: 4px 0 0;
81
+ color: #6b7280;
82
+ }
83
+
84
+ .review-carousel__controls {
85
+ display: flex;
86
+ gap: 8px;
87
+ }
88
+
89
+ .review-carousel__button {
90
+ width: 40px;
91
+ height: 40px;
92
+ border: 1px solid #d1d5db;
93
+ border-radius: 999px;
94
+ color: #111827;
95
+ background: #ffffff;
96
+ cursor: pointer;
97
+ }
98
+
99
+ .review-carousel__button:hover {
100
+ border-color: #4f46e5;
101
+ color: #4f46e5;
102
+ }
103
+
104
+ .fluid-widget-builder-preview {
105
+ display: grid;
106
+ gap: 32px;
107
+ }
108
+
109
+ .fluid-widget-builder-preview__header {
110
+ max-width: 720px;
111
+ margin: 0 auto;
112
+ text-align: center;
113
+ }
114
+
115
+ .fluid-widget-builder-preview__eyebrow {
116
+ margin: 0 0 8px;
117
+ color: #4f46e5;
118
+ font-size: 0.75rem;
119
+ font-weight: 700;
120
+ letter-spacing: 0.08em;
121
+ text-transform: uppercase;
122
+ }
123
+
124
+ .fluid-widget-builder-preview__title {
125
+ margin: 0;
126
+ color: #111827;
127
+ font-size: clamp(2.5rem, 5vw, 4.5rem);
128
+ line-height: 0.95;
129
+ letter-spacing: -0.06em;
130
+ }
131
+
132
+ .fluid-widget-builder-preview__description {
133
+ margin: 16px 0 0;
134
+ color: #64748b;
135
+ font-size: 1rem;
136
+ }
137
+
138
+ .fluid-widget-builder-preview__grid {
139
+ display: grid;
140
+ gap: 24px;
141
+ }
142
+
143
+ .fluid-widget-builder-preview__card {
144
+ display: grid;
145
+ gap: 16px;
146
+ }
147
+
148
+ .fluid-widget-builder-preview__meta {
149
+ display: grid;
150
+ gap: 6px;
151
+ max-width: 960px;
152
+ margin: 0 auto;
153
+ color: #64748b;
154
+ }
155
+
156
+ .fluid-widget-builder-preview__meta p {
157
+ margin: 0;
158
+ }
159
+
160
+ .fluid-widget-builder-preview__meta code {
161
+ color: #4338ca;
162
+ }
163
+
164
+ .fluid-widget-builder-preview__widget-name {
165
+ color: #111827;
166
+ font-weight: 700;
167
+ }
168
+
169
+ .fluid-widget-builder-preview__surface {
170
+ max-width: 960px;
171
+ width: 100%;
172
+ margin: 0 auto;
173
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src", "fluid.widget.config.ts", "vite.config.ts"],
20
+ "references": []
21
+ }