@fewchurs/react 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,41 @@
1
+ Fewchurs SDK License
2
+
3
+ Copyright (c) 2026 Fewchurs. All rights reserved.
4
+
5
+ This software and its source code (the "SDK") are proprietary to Fewchurs.
6
+ The source code is published for transparency only. It is not open source.
7
+
8
+ 1. Permitted use. You may install the SDK, unmodified, as an npm
9
+ dependency of your own applications, and deploy it as part of those
10
+ applications, including in the bundles they serve to browsers, solely to
11
+ connect them to the Fewchurs service under a valid Fewchurs account. You
12
+ may customise its look and text through the props, CSS variables, slots,
13
+ hooks and stylesheet overrides the SDK documents.
14
+
15
+ 2. Restrictions. Except as allowed in section 1, you may not:
16
+ a. copy, modify, translate or create derivative works of the SDK;
17
+ b. distribute, sublicense, sell, rent or lease the SDK on its own or as
18
+ part of a library, SDK, framework or developer tool;
19
+ c. use the SDK, or any part of it, to build or run a product or service
20
+ that competes with Fewchurs, or to connect to any service other than
21
+ Fewchurs;
22
+ d. remove or alter any copyright or license notice in the SDK.
23
+
24
+ 3. Ownership. Fewchurs keeps all rights, title and interest in the SDK. No
25
+ rights are granted except those stated in this license.
26
+
27
+ 4. Termination. This license ends automatically if you breach it, or when
28
+ your Fewchurs account ends. On termination you must stop deploying new
29
+ versions of your applications that include the SDK.
30
+
31
+ 5. No warranty. THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
33
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
34
+
35
+ 6. Limitation of liability. IN NO EVENT SHALL FEWCHURS BE LIABLE FOR ANY
36
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
37
+ OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SDK OR THE USE
38
+ OF OR OTHER DEALINGS IN THE SDK.
39
+
40
+ Your use of the Fewchurs service itself is governed separately by your
41
+ agreement with Fewchurs (https://www.fewchurs.shop).
package/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @fewchurs/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Fewchurs feature-request board for any React app — the client, the store, the hooks and the
4
+ components. See the [repository README](../../README.md) for the full guide.
5
+
6
+ ```bash
7
+ pnpm add @fewchurs/react
8
+ ```
9
+
10
+ ```tsx
11
+ import { FewchursBoard, FewchursProvider } from "@fewchurs/react";
12
+ import "@fewchurs/react/styles.css";
13
+
14
+ <FewchursProvider apiKey={import.meta.env.VITE_FEWCHURS_KEY}>
15
+ <FewchursBoard />
16
+ </FewchursProvider>;
17
+ ```
18
+
19
+ `FewchursBoardDialog` renders the same board in a modal (`open`, `onClose`, plus every board
20
+ prop), and `initialFeatureId` opens the board on one request with its comments expanded.
21
+
22
+ In a Next.js app, use [`@fewchurs/next`](../next) instead: it renders the board on the server
23
+ and keeps the API key out of the browser.
24
+
25
+ `@fewchurs/react/core` is the server-safe half — the fetch client, the types, validation and
26
+ error mapping, with no React and no `"use client"` boundary.
@@ -0,0 +1,213 @@
1
+ type FewchursError = {
2
+ code: "invalid_key";
3
+ status: 401;
4
+ message: string;
5
+ } | {
6
+ code: "validation_failed";
7
+ status: 400;
8
+ message: string;
9
+ } | {
10
+ code: "rate_limited";
11
+ status: 429;
12
+ message: string;
13
+ } | {
14
+ code: "not_found";
15
+ status: 404;
16
+ message: string;
17
+ } | {
18
+ code: "limit_reached";
19
+ status: 402;
20
+ message: string;
21
+ } | {
22
+ code: "server";
23
+ status: number;
24
+ message: string;
25
+ } | {
26
+ code: "network";
27
+ message: string;
28
+ };
29
+ type Result<T> = {
30
+ ok: true;
31
+ data: T;
32
+ } | {
33
+ ok: false;
34
+ error: FewchursError;
35
+ };
36
+ /** Safe to show a visitor: never leaks a server message that may name internals. */
37
+ declare function userMessage(error: FewchursError): string;
38
+ declare function networkError(message?: string): FewchursError;
39
+ declare function validationError(message: string): FewchursError;
40
+ declare function errorFromResponse(status: number, body: unknown): FewchursError;
41
+
42
+ /** A status the server does not know about yet still renders, as `"unknown"`. */
43
+ type FeatureStatus = "open" | "planned" | "in_progress" | "done" | "declined" | "unknown";
44
+ type FeatureSort = "top" | "new";
45
+ interface Feature {
46
+ id: string;
47
+ title: string;
48
+ description: string;
49
+ status: FeatureStatus;
50
+ upvoteCount: number;
51
+ commentCount: number;
52
+ followerCount: number;
53
+ hasVoted: boolean;
54
+ isFollowing: boolean;
55
+ isMine: boolean;
56
+ createdAt: string;
57
+ updatedAt: string;
58
+ }
59
+ interface Comment {
60
+ id: string;
61
+ text: string;
62
+ authorName: string;
63
+ isDeveloper: boolean;
64
+ isMine: boolean;
65
+ createdAt: string;
66
+ }
67
+ interface FeaturePage {
68
+ features: Feature[];
69
+ nextCursor: string | null;
70
+ }
71
+ interface CommentPage {
72
+ comments: Comment[];
73
+ nextCursor: string | null;
74
+ }
75
+ interface FewchursConfig {
76
+ showBranding: boolean;
77
+ hideVoteCounts: boolean;
78
+ appName: string | null;
79
+ }
80
+ interface SubmitFeatureInput {
81
+ title: string;
82
+ description?: string;
83
+ email?: string;
84
+ isSubscriber?: boolean;
85
+ }
86
+ interface CreateCommentInput {
87
+ featureId: string;
88
+ text: string;
89
+ authorName?: string;
90
+ }
91
+ interface ListFeaturesParams {
92
+ sort?: FeatureSort;
93
+ limit?: number;
94
+ cursor?: string | null;
95
+ status?: FeatureStatus[];
96
+ }
97
+ interface ListCommentsParams {
98
+ limit?: number;
99
+ cursor?: string | null;
100
+ }
101
+
102
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
103
+ interface FewchursClientOptions {
104
+ /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */
105
+ apiKey?: string;
106
+ /** `https://www.fewchurs.shop/api/v1` by default, or a proxy path such as `/api/fewchurs`. */
107
+ baseUrl?: string;
108
+ deviceId?: string | null | (() => string | null);
109
+ /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */
110
+ language?: string;
111
+ fetch?: FetchLike;
112
+ retries?: number;
113
+ retryDelayMs?: number;
114
+ }
115
+ interface FewchursClient {
116
+ readonly baseUrl: string;
117
+ getDeviceId(): string | null;
118
+ getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;
119
+ listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;
120
+ getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;
121
+ submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;
122
+ vote(featureId: string): Promise<Result<{
123
+ upvoteCount: number;
124
+ hasVoted: boolean;
125
+ }>>;
126
+ unvote(featureId: string): Promise<Result<{
127
+ upvoteCount: number;
128
+ hasVoted: boolean;
129
+ }>>;
130
+ follow(featureId: string, email: string): Promise<Result<{
131
+ following: boolean;
132
+ }>>;
133
+ unfollow(featureId: string): Promise<Result<{
134
+ following: boolean;
135
+ }>>;
136
+ listComments(featureId: string, params?: ListCommentsParams, signal?: AbortSignal): Promise<Result<CommentPage>>;
137
+ createComment(input: CreateCommentInput): Promise<Result<Comment>>;
138
+ }
139
+ declare function createFewchursClient(options?: FewchursClientOptions): FewchursClient;
140
+
141
+ declare const SDK_VERSION = "1.0.0";
142
+
143
+ declare const SDK_HEADER = "react/1.0.0";
144
+ declare const DEFAULT_BASE_URL = "https://www.fewchurs.shop/api/v1";
145
+ declare const DEFAULT_PAGE_SIZE = 20;
146
+ /** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
147
+ declare const LIMITS: {
148
+ readonly titleMin: 3;
149
+ readonly titleMax: 100;
150
+ readonly descriptionMax: 1000;
151
+ readonly commentMin: 1;
152
+ readonly commentMax: 1000;
153
+ readonly authorNameMax: 60;
154
+ };
155
+ /** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */
156
+ declare const ROADMAP_STATUSES: FeatureStatus[];
157
+
158
+ declare function createDeviceId(): string;
159
+
160
+ interface ListQuery {
161
+ sort: FeatureSort;
162
+ status?: FeatureStatus[];
163
+ }
164
+ declare function listKey({ sort, status }: ListQuery): string;
165
+ /**
166
+ * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked
167
+ * request goes through the same vote, comment and follow updates as a row in a list.
168
+ */
169
+ declare function featureKey(featureId: string): string;
170
+
171
+ type LoadStatus = "idle" | "loading" | "loadingMore" | "ready" | "error";
172
+ interface ListState {
173
+ features: Feature[];
174
+ cursor: string | null;
175
+ status: LoadStatus;
176
+ error: FewchursError | null;
177
+ }
178
+ interface CommentsState {
179
+ comments: Comment[];
180
+ cursor: string | null;
181
+ status: LoadStatus;
182
+ error: FewchursError | null;
183
+ }
184
+ interface StoreState {
185
+ config: FewchursConfig | null;
186
+ lists: Record<string, ListState>;
187
+ comments: Record<string, CommentsState>;
188
+ }
189
+ type FewchursEventName = "boardOpened" | "featureSubmitted" | "voted" | "commented" | "followed" | "error";
190
+ interface FewchursEvent {
191
+ name: FewchursEventName;
192
+ featureId?: string;
193
+ hasVoted?: boolean;
194
+ following?: boolean;
195
+ error?: FewchursError;
196
+ }
197
+
198
+ declare function normalizeStatus(value: unknown): FeatureStatus;
199
+ declare function normalizeFeature(value: unknown, deviceId: string | null): Feature;
200
+ declare function normalizeComment(value: unknown, deviceId: string | null): Comment;
201
+ declare function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage;
202
+ declare function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage;
203
+ declare function normalizeConfig(value: unknown): FewchursConfig;
204
+ declare function normalizeVote(value: unknown): {
205
+ upvoteCount: number;
206
+ hasVoted: boolean;
207
+ };
208
+
209
+ declare function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null;
210
+ declare function validateCommentInput(input: CreateCommentInput): FewchursError | null;
211
+ declare function validateEmail(email: string): FewchursError | null;
212
+
213
+ export { type Comment, type CommentPage, type CommentsState, type CreateCommentInput, DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, type Feature, type FeaturePage, type FeatureSort, type FeatureStatus, type FetchLike, type FewchursClient, type FewchursClientOptions, type FewchursConfig, type FewchursError, type FewchursEvent, type FewchursEventName, LIMITS, type ListCommentsParams, type ListFeaturesParams, type ListQuery, type ListState, type LoadStatus, ROADMAP_STATUSES, type Result, SDK_HEADER, SDK_VERSION, type StoreState, type SubmitFeatureInput, createDeviceId, createFewchursClient, errorFromResponse, featureKey, listKey, networkError, normalizeComment, normalizeCommentPage, normalizeConfig, normalizeFeature, normalizeFeaturePage, normalizeStatus, normalizeVote, userMessage, validateCommentInput, validateEmail, validateFeatureInput, validationError };
package/dist/core.d.ts ADDED
@@ -0,0 +1,213 @@
1
+ type FewchursError = {
2
+ code: "invalid_key";
3
+ status: 401;
4
+ message: string;
5
+ } | {
6
+ code: "validation_failed";
7
+ status: 400;
8
+ message: string;
9
+ } | {
10
+ code: "rate_limited";
11
+ status: 429;
12
+ message: string;
13
+ } | {
14
+ code: "not_found";
15
+ status: 404;
16
+ message: string;
17
+ } | {
18
+ code: "limit_reached";
19
+ status: 402;
20
+ message: string;
21
+ } | {
22
+ code: "server";
23
+ status: number;
24
+ message: string;
25
+ } | {
26
+ code: "network";
27
+ message: string;
28
+ };
29
+ type Result<T> = {
30
+ ok: true;
31
+ data: T;
32
+ } | {
33
+ ok: false;
34
+ error: FewchursError;
35
+ };
36
+ /** Safe to show a visitor: never leaks a server message that may name internals. */
37
+ declare function userMessage(error: FewchursError): string;
38
+ declare function networkError(message?: string): FewchursError;
39
+ declare function validationError(message: string): FewchursError;
40
+ declare function errorFromResponse(status: number, body: unknown): FewchursError;
41
+
42
+ /** A status the server does not know about yet still renders, as `"unknown"`. */
43
+ type FeatureStatus = "open" | "planned" | "in_progress" | "done" | "declined" | "unknown";
44
+ type FeatureSort = "top" | "new";
45
+ interface Feature {
46
+ id: string;
47
+ title: string;
48
+ description: string;
49
+ status: FeatureStatus;
50
+ upvoteCount: number;
51
+ commentCount: number;
52
+ followerCount: number;
53
+ hasVoted: boolean;
54
+ isFollowing: boolean;
55
+ isMine: boolean;
56
+ createdAt: string;
57
+ updatedAt: string;
58
+ }
59
+ interface Comment {
60
+ id: string;
61
+ text: string;
62
+ authorName: string;
63
+ isDeveloper: boolean;
64
+ isMine: boolean;
65
+ createdAt: string;
66
+ }
67
+ interface FeaturePage {
68
+ features: Feature[];
69
+ nextCursor: string | null;
70
+ }
71
+ interface CommentPage {
72
+ comments: Comment[];
73
+ nextCursor: string | null;
74
+ }
75
+ interface FewchursConfig {
76
+ showBranding: boolean;
77
+ hideVoteCounts: boolean;
78
+ appName: string | null;
79
+ }
80
+ interface SubmitFeatureInput {
81
+ title: string;
82
+ description?: string;
83
+ email?: string;
84
+ isSubscriber?: boolean;
85
+ }
86
+ interface CreateCommentInput {
87
+ featureId: string;
88
+ text: string;
89
+ authorName?: string;
90
+ }
91
+ interface ListFeaturesParams {
92
+ sort?: FeatureSort;
93
+ limit?: number;
94
+ cursor?: string | null;
95
+ status?: FeatureStatus[];
96
+ }
97
+ interface ListCommentsParams {
98
+ limit?: number;
99
+ cursor?: string | null;
100
+ }
101
+
102
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
103
+ interface FewchursClientOptions {
104
+ /** Server-side, or in a SPA where the key is publishable. Omit when calling your own proxy. */
105
+ apiKey?: string;
106
+ /** `https://www.fewchurs.shop/api/v1` by default, or a proxy path such as `/api/fewchurs`. */
107
+ baseUrl?: string;
108
+ deviceId?: string | null | (() => string | null);
109
+ /** Sent as `Accept-Language`, so server-side copy and emails match the visitor. */
110
+ language?: string;
111
+ fetch?: FetchLike;
112
+ retries?: number;
113
+ retryDelayMs?: number;
114
+ }
115
+ interface FewchursClient {
116
+ readonly baseUrl: string;
117
+ getDeviceId(): string | null;
118
+ getConfig(signal?: AbortSignal): Promise<Result<FewchursConfig>>;
119
+ listFeatures(params?: ListFeaturesParams, signal?: AbortSignal): Promise<Result<FeaturePage>>;
120
+ getFeature(featureId: string, signal?: AbortSignal): Promise<Result<Feature>>;
121
+ submitFeature(input: SubmitFeatureInput): Promise<Result<Feature>>;
122
+ vote(featureId: string): Promise<Result<{
123
+ upvoteCount: number;
124
+ hasVoted: boolean;
125
+ }>>;
126
+ unvote(featureId: string): Promise<Result<{
127
+ upvoteCount: number;
128
+ hasVoted: boolean;
129
+ }>>;
130
+ follow(featureId: string, email: string): Promise<Result<{
131
+ following: boolean;
132
+ }>>;
133
+ unfollow(featureId: string): Promise<Result<{
134
+ following: boolean;
135
+ }>>;
136
+ listComments(featureId: string, params?: ListCommentsParams, signal?: AbortSignal): Promise<Result<CommentPage>>;
137
+ createComment(input: CreateCommentInput): Promise<Result<Comment>>;
138
+ }
139
+ declare function createFewchursClient(options?: FewchursClientOptions): FewchursClient;
140
+
141
+ declare const SDK_VERSION = "1.0.0";
142
+
143
+ declare const SDK_HEADER = "react/1.0.0";
144
+ declare const DEFAULT_BASE_URL = "https://www.fewchurs.shop/api/v1";
145
+ declare const DEFAULT_PAGE_SIZE = 20;
146
+ /** Mirrors the server's zod schemas, so the UI can reject input before a round trip. */
147
+ declare const LIMITS: {
148
+ readonly titleMin: 3;
149
+ readonly titleMax: 100;
150
+ readonly descriptionMax: 1000;
151
+ readonly commentMin: 1;
152
+ readonly commentMax: 1000;
153
+ readonly authorNameMax: 60;
154
+ };
155
+ /** What the Roadmap tab asks for, and what it keeps if the server ignores the filter. */
156
+ declare const ROADMAP_STATUSES: FeatureStatus[];
157
+
158
+ declare function createDeviceId(): string;
159
+
160
+ interface ListQuery {
161
+ sort: FeatureSort;
162
+ status?: FeatureStatus[];
163
+ }
164
+ declare function listKey({ sort, status }: ListQuery): string;
165
+ /**
166
+ * One feature, fetched by id, kept in `lists` beside the real lists. That way a deep-linked
167
+ * request goes through the same vote, comment and follow updates as a row in a list.
168
+ */
169
+ declare function featureKey(featureId: string): string;
170
+
171
+ type LoadStatus = "idle" | "loading" | "loadingMore" | "ready" | "error";
172
+ interface ListState {
173
+ features: Feature[];
174
+ cursor: string | null;
175
+ status: LoadStatus;
176
+ error: FewchursError | null;
177
+ }
178
+ interface CommentsState {
179
+ comments: Comment[];
180
+ cursor: string | null;
181
+ status: LoadStatus;
182
+ error: FewchursError | null;
183
+ }
184
+ interface StoreState {
185
+ config: FewchursConfig | null;
186
+ lists: Record<string, ListState>;
187
+ comments: Record<string, CommentsState>;
188
+ }
189
+ type FewchursEventName = "boardOpened" | "featureSubmitted" | "voted" | "commented" | "followed" | "error";
190
+ interface FewchursEvent {
191
+ name: FewchursEventName;
192
+ featureId?: string;
193
+ hasVoted?: boolean;
194
+ following?: boolean;
195
+ error?: FewchursError;
196
+ }
197
+
198
+ declare function normalizeStatus(value: unknown): FeatureStatus;
199
+ declare function normalizeFeature(value: unknown, deviceId: string | null): Feature;
200
+ declare function normalizeComment(value: unknown, deviceId: string | null): Comment;
201
+ declare function normalizeFeaturePage(value: unknown, deviceId: string | null): FeaturePage;
202
+ declare function normalizeCommentPage(value: unknown, deviceId: string | null): CommentPage;
203
+ declare function normalizeConfig(value: unknown): FewchursConfig;
204
+ declare function normalizeVote(value: unknown): {
205
+ upvoteCount: number;
206
+ hasVoted: boolean;
207
+ };
208
+
209
+ declare function validateFeatureInput(input: SubmitFeatureInput): FewchursError | null;
210
+ declare function validateCommentInput(input: CreateCommentInput): FewchursError | null;
211
+ declare function validateEmail(email: string): FewchursError | null;
212
+
213
+ export { type Comment, type CommentPage, type CommentsState, type CreateCommentInput, DEFAULT_BASE_URL, DEFAULT_PAGE_SIZE, type Feature, type FeaturePage, type FeatureSort, type FeatureStatus, type FetchLike, type FewchursClient, type FewchursClientOptions, type FewchursConfig, type FewchursError, type FewchursEvent, type FewchursEventName, LIMITS, type ListCommentsParams, type ListFeaturesParams, type ListQuery, type ListState, type LoadStatus, ROADMAP_STATUSES, type Result, SDK_HEADER, SDK_VERSION, type StoreState, type SubmitFeatureInput, createDeviceId, createFewchursClient, errorFromResponse, featureKey, listKey, networkError, normalizeComment, normalizeCommentPage, normalizeConfig, normalizeFeature, normalizeFeaturePage, normalizeStatus, normalizeVote, userMessage, validateCommentInput, validateEmail, validateFeatureInput, validationError };