anton-bakker-feedback 0.3.0 → 0.3.2

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,32 @@
1
+ import { StoredFeedbackItem, FeedbackStatus } from '../core/types.js';
2
+
3
+ export interface FeedbackServiceConfig {
4
+ tableName: string;
5
+ region: string;
6
+ }
7
+ export interface ListOptions {
8
+ status?: FeedbackStatus;
9
+ limit?: number;
10
+ lastEvaluatedKey?: Record<string, unknown>;
11
+ }
12
+ export declare class FeedbackService {
13
+ private readonly tableName;
14
+ private readonly region;
15
+ constructor(config: FeedbackServiceConfig);
16
+ /**
17
+ * PutItem — writes a StoredFeedbackItem to DynamoDB.
18
+ * No-op when tableName is absent.
19
+ */
20
+ put(item: StoredFeedbackItem): Promise<void>;
21
+ /**
22
+ * GetItem — retrieves a StoredFeedbackItem by PK+SK.
23
+ * Returns null if the item does not exist.
24
+ */
25
+ get(pk: string, sk: string): Promise<StoredFeedbackItem | null>;
26
+ /**
27
+ * Query GSI1 with optional status filter.
28
+ * Returns [] when tableName is absent (Minimal level).
29
+ */
30
+ list(opts?: ListOptions): Promise<StoredFeedbackItem[]>;
31
+ }
32
+ //# sourceMappingURL=FeedbackService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeedbackService.d.ts","sourceRoot":"","sources":["../../src/server/FeedbackService.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAM3E,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AA4ED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,qBAAqB;IAOzC;;;OAGG;IACG,GAAG,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBlD;;;OAGG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IA6BrE;;;OAGG;IACG,IAAI,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;CAmDlE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * server/index.ts — Re-exports for the `./server` package entry point.
3
+ *
4
+ * Exports:
5
+ * - FeedbackService (DynamoDB CRUD)
6
+ * - createListHandler (HTTP GET /api/feedback/list)
7
+ * - createIntakeResolver (re-export from submitResolver.ts)
8
+ */
9
+ export { FeedbackService } from './FeedbackService.js';
10
+ export type { FeedbackServiceConfig, ListOptions } from './FeedbackService.js';
11
+ export { createListHandler } from './listHandler.js';
12
+ export type { ListHandlerConfig } from './listHandler.js';
13
+ export { createIntakeResolver } from '../react/resolver/submitResolver.js';
14
+ export type { RequestHandler, RequestBody, ResponseBody, SubmitHandlerConfig, SubmitPayload } from '../react/resolver/submitResolver.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAC3E,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { RequestHandler } from '../react/resolver/submitResolver.js';
2
+
3
+ export interface ListHandlerConfig {
4
+ /** DynamoDB table name — if absent, returns [] (Minimal level) */
5
+ tableName?: string;
6
+ /** AWS region for DynamoDB */
7
+ region?: string;
8
+ }
9
+ /**
10
+ * Creates an Express/Lambda-compatible request handler for the list endpoint.
11
+ *
12
+ * Validates query params → queries DynamoDB GSI1 → returns paginated results.
13
+ * Returns 400 for invalid status values.
14
+ * Returns 200 with items array (empty [] when no tableName configured).
15
+ */
16
+ export declare function createListHandler(config: ListHandlerConfig): RequestHandler;
17
+ //# sourceMappingURL=listHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listHandler.d.ts","sourceRoot":"","sources":["../../src/server/listHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAM1E,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAYD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAkD3E"}
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export {};
1
+ export * from './server/index.js';
2
2
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA,cAAc,mBAAmB,CAAC"}
package/dist/server.js CHANGED
@@ -1,2 +1,203 @@
1
-
1
+ import { c } from "./chunks/submitResolver-DsfkrUHY.js";
2
+ function toAttr(value) {
3
+ if (value === null || value === void 0) {
4
+ return { NULL: true };
5
+ }
6
+ if (typeof value === "string") {
7
+ return { S: value };
8
+ }
9
+ if (typeof value === "number") {
10
+ return { N: String(value) };
11
+ }
12
+ if (typeof value === "boolean") {
13
+ return { BOOL: value };
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return { L: value.map(toAttr) };
17
+ }
18
+ if (typeof value === "object") {
19
+ const m = {};
20
+ for (const [k, v] of Object.entries(value)) {
21
+ m[k] = toAttr(v);
22
+ }
23
+ return { M: m };
24
+ }
25
+ return { S: String(value) };
26
+ }
27
+ function marshalItem(item) {
28
+ const result = {};
29
+ for (const [key, value] of Object.entries(item)) {
30
+ if (value !== void 0) {
31
+ result[key] = toAttr(value);
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ function fromAttr(attr) {
37
+ if (attr["S"] !== void 0) return attr["S"];
38
+ if (attr["N"] !== void 0) return Number(attr["N"]);
39
+ if (attr["BOOL"] !== void 0) return attr["BOOL"];
40
+ if (attr["NULL"]) return null;
41
+ if (attr["L"]) {
42
+ return attr["L"].map(fromAttr);
43
+ }
44
+ if (attr["M"]) {
45
+ const obj = {};
46
+ for (const [k, v] of Object.entries(attr["M"])) {
47
+ obj[k] = fromAttr(v);
48
+ }
49
+ return obj;
50
+ }
51
+ return void 0;
52
+ }
53
+ function unmarshalItem(item) {
54
+ const result = {};
55
+ for (const [key, attr] of Object.entries(item)) {
56
+ result[key] = fromAttr(attr);
57
+ }
58
+ return result;
59
+ }
60
+ class FeedbackService {
61
+ tableName;
62
+ region;
63
+ constructor(config) {
64
+ this.tableName = config.tableName;
65
+ this.region = config.region;
66
+ }
67
+ // ── put ────────────────────────────────────────────────────────
68
+ /**
69
+ * PutItem — writes a StoredFeedbackItem to DynamoDB.
70
+ * No-op when tableName is absent.
71
+ */
72
+ async put(item) {
73
+ if (!this.tableName) {
74
+ return;
75
+ }
76
+ const dynamoPrefix = ["@aws", "-sdk/client-dynamo", "db"].join("");
77
+ const { DynamoDBClient, PutItemCommand } = await import(
78
+ /* @vite-ignore */
79
+ dynamoPrefix
80
+ );
81
+ const client = new DynamoDBClient({ region: this.region });
82
+ await client.send(
83
+ new PutItemCommand({
84
+ TableName: this.tableName,
85
+ Item: marshalItem(item)
86
+ })
87
+ );
88
+ }
89
+ // ── get ────────────────────────────────────────────────────────
90
+ /**
91
+ * GetItem — retrieves a StoredFeedbackItem by PK+SK.
92
+ * Returns null if the item does not exist.
93
+ */
94
+ async get(pk, sk) {
95
+ if (!this.tableName) {
96
+ return null;
97
+ }
98
+ const dynamoPrefix = ["@aws", "-sdk/client-dynamo", "db"].join("");
99
+ const { DynamoDBClient, GetItemCommand } = await import(
100
+ /* @vite-ignore */
101
+ dynamoPrefix
102
+ );
103
+ const client = new DynamoDBClient({ region: this.region });
104
+ const response = await client.send(
105
+ new GetItemCommand({
106
+ TableName: this.tableName,
107
+ Key: {
108
+ PK: { S: pk },
109
+ SK: { S: sk }
110
+ }
111
+ })
112
+ );
113
+ if (!response.Item) {
114
+ return null;
115
+ }
116
+ return unmarshalItem(response.Item);
117
+ }
118
+ // ── list ───────────────────────────────────────────────────────
119
+ /**
120
+ * Query GSI1 with optional status filter.
121
+ * Returns [] when tableName is absent (Minimal level).
122
+ */
123
+ async list(opts = {}) {
124
+ if (!this.tableName) {
125
+ return [];
126
+ }
127
+ const dynamoPrefix = ["@aws", "-sdk/client-dynamo", "db"].join("");
128
+ const { DynamoDBClient, QueryCommand } = await import(
129
+ /* @vite-ignore */
130
+ dynamoPrefix
131
+ );
132
+ const client = new DynamoDBClient({ region: this.region });
133
+ const limit = opts.limit ?? 50;
134
+ const gsi1pk = opts.status ? `STATUS#${opts.status}` : void 0;
135
+ const params = {
136
+ TableName: this.tableName,
137
+ IndexName: "GSI1",
138
+ KeyConditionExpression: "#pk = :pk",
139
+ ExpressionAttributeNames: {
140
+ "#pk": "GSI1PK"
141
+ },
142
+ ExpressionAttributeValues: {
143
+ ":pk": { S: gsi1pk ?? "STATUS#open" }
144
+ },
145
+ Limit: limit,
146
+ ScanIndexForward: false
147
+ // Descending (highest priority first via inverted sort key)
148
+ };
149
+ if (opts.lastEvaluatedKey) {
150
+ params.ExclusiveStartKey = opts.lastEvaluatedKey;
151
+ }
152
+ const response = await client.send(new QueryCommand(params));
153
+ if (!response.Items || response.Items.length === 0) {
154
+ return [];
155
+ }
156
+ return response.Items.map(unmarshalItem);
157
+ }
158
+ }
159
+ const VALID_STATUSES = ["open", "approved", "deferred", "resolved", "wontfix"];
160
+ function createListHandler(config) {
161
+ const service = new FeedbackService({
162
+ tableName: config.tableName ?? "",
163
+ region: config.region ?? "us-east-1"
164
+ });
165
+ return async (req, res) => {
166
+ const errors = [];
167
+ const query = req.query ?? {};
168
+ const rawStatus = query["status"];
169
+ const rawLimit = query["limit"];
170
+ let status;
171
+ if (rawStatus !== void 0) {
172
+ if (!VALID_STATUSES.includes(rawStatus)) {
173
+ errors.push(`status must be one of: ${VALID_STATUSES.join(", ")} (got: ${rawStatus})`);
174
+ } else {
175
+ status = rawStatus;
176
+ }
177
+ }
178
+ let limit;
179
+ if (rawLimit !== void 0) {
180
+ const parsed = parseInt(rawLimit, 10);
181
+ if (isNaN(parsed) || parsed < 1 || parsed > 100) {
182
+ errors.push("limit must be a number between 1 and 100");
183
+ } else {
184
+ limit = parsed;
185
+ }
186
+ }
187
+ if (errors.length > 0) {
188
+ res.status(400).json({ errors });
189
+ return;
190
+ }
191
+ const opts = {};
192
+ if (status !== void 0) opts.status = status;
193
+ if (limit !== void 0) opts.limit = limit;
194
+ const items = await service.list(opts);
195
+ res.status(200).json({ items });
196
+ };
197
+ }
198
+ export {
199
+ FeedbackService,
200
+ c as createIntakeResolver,
201
+ createListHandler
202
+ };
2
203
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
1
+ {"version":3,"file":"server.js","sources":["../src/server/FeedbackService.ts","../src/server/listHandler.ts"],"sourcesContent":["/**\n * FeedbackService — DynamoDB CRUD operations for feedback items.\n *\n * Server-only module — NOT imported by the client bundle.\n * Uses dynamic import for @aws-sdk/client-dynamodb to avoid hard client bundle dep\n * (same pattern as submitResolver.ts).\n *\n * When tableName is absent (Minimal level), put is a no-op and list returns [].\n */\n\nimport type { StoredFeedbackItem, FeedbackStatus } from '../core/types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface FeedbackServiceConfig {\n tableName: string;\n region: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ListOptions\n// ─────────────────────────────────────────────────────────────────\n\nexport interface ListOptions {\n status?: FeedbackStatus;\n limit?: number;\n lastEvaluatedKey?: Record<string, unknown>;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────\n\n/** Marshal a JS value to a DynamoDB AttributeValue */\nfunction toAttr(value: unknown): Record<string, unknown> {\n if (value === null || value === undefined) {\n return { NULL: true };\n }\n if (typeof value === 'string') {\n return { S: value };\n }\n if (typeof value === 'number') {\n return { N: String(value) };\n }\n if (typeof value === 'boolean') {\n return { BOOL: value };\n }\n if (Array.isArray(value)) {\n return { L: value.map(toAttr) };\n }\n if (typeof value === 'object') {\n const m: Record<string, Record<string, unknown>> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n m[k] = toAttr(v);\n }\n return { M: m };\n }\n return { S: String(value) };\n}\n\n/** Marshal the entire StoredFeedbackItem to a DynamoDB Item map */\nfunction marshalItem(item: StoredFeedbackItem): Record<string, Record<string, unknown>> {\n const result: Record<string, Record<string, unknown>> = {};\n for (const [key, value] of Object.entries(item as unknown as Record<string, unknown>)) {\n if (value !== undefined) {\n result[key] = toAttr(value);\n }\n }\n return result;\n}\n\n/** Unmarshal a DynamoDB AttributeValue to a JS value */\nfunction fromAttr(attr: Record<string, unknown>): unknown {\n if (attr['S'] !== undefined) return attr['S'];\n if (attr['N'] !== undefined) return Number(attr['N']);\n if (attr['BOOL'] !== undefined) return attr['BOOL'];\n if (attr['NULL']) return null;\n if (attr['L']) {\n return (attr['L'] as Array<Record<string, unknown>>).map(fromAttr);\n }\n if (attr['M']) {\n const obj: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(attr['M'] as Record<string, Record<string, unknown>>)) {\n obj[k] = fromAttr(v);\n }\n return obj;\n }\n return undefined;\n}\n\n/** Unmarshal a DynamoDB Item map to StoredFeedbackItem */\nfunction unmarshalItem(item: Record<string, Record<string, unknown>>): StoredFeedbackItem {\n const result: Record<string, unknown> = {};\n for (const [key, attr] of Object.entries(item)) {\n result[key] = fromAttr(attr);\n }\n return result as unknown as StoredFeedbackItem;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// FeedbackService\n// ─────────────────────────────────────────────────────────────────\n\nexport class FeedbackService {\n private readonly tableName: string;\n private readonly region: string;\n\n constructor(config: FeedbackServiceConfig) {\n this.tableName = config.tableName;\n this.region = config.region;\n }\n\n // ── put ────────────────────────────────────────────────────────\n\n /**\n * PutItem — writes a StoredFeedbackItem to DynamoDB.\n * No-op when tableName is absent.\n */\n async put(item: StoredFeedbackItem): Promise<void> {\n if (!this.tableName) {\n return;\n }\n\n // Dynamic import to avoid hard dependency on AWS SDK in client bundle\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, PutItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n await client.send(\n new PutItemCommand({\n TableName: this.tableName,\n Item: marshalItem(item),\n }),\n );\n }\n\n // ── get ────────────────────────────────────────────────────────\n\n /**\n * GetItem — retrieves a StoredFeedbackItem by PK+SK.\n * Returns null if the item does not exist.\n */\n async get(pk: string, sk: string): Promise<StoredFeedbackItem | null> {\n if (!this.tableName) {\n return null;\n }\n\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, GetItemCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n const response = await client.send(\n new GetItemCommand({\n TableName: this.tableName,\n Key: {\n PK: { S: pk },\n SK: { S: sk },\n },\n }),\n );\n\n if (!response.Item) {\n return null;\n }\n\n return unmarshalItem(response.Item as Record<string, Record<string, unknown>>);\n }\n\n // ── list ───────────────────────────────────────────────────────\n\n /**\n * Query GSI1 with optional status filter.\n * Returns [] when tableName is absent (Minimal level).\n */\n async list(opts: ListOptions = {}): Promise<StoredFeedbackItem[]> {\n if (!this.tableName) {\n return [];\n }\n\n const dynamoPrefix = ['@aws', '-sdk/client-dynamo', 'db'].join('');\n const { DynamoDBClient, QueryCommand } = await import(/* @vite-ignore */ dynamoPrefix);\n\n const client = new DynamoDBClient({ region: this.region });\n\n const limit = opts.limit ?? 50;\n const gsi1pk = opts.status ? `STATUS#${opts.status}` : undefined;\n\n // Build query params\n type QueryParams = {\n TableName: string;\n IndexName: string;\n KeyConditionExpression: string;\n ExpressionAttributeNames: Record<string, string>;\n ExpressionAttributeValues: Record<string, Record<string, unknown>>;\n Limit: number;\n ExclusiveStartKey?: Record<string, unknown>;\n ScanIndexForward: boolean;\n };\n\n const params: QueryParams = {\n TableName: this.tableName,\n IndexName: 'GSI1',\n KeyConditionExpression: '#pk = :pk',\n ExpressionAttributeNames: {\n '#pk': 'GSI1PK',\n },\n ExpressionAttributeValues: {\n ':pk': { S: gsi1pk ?? 'STATUS#open' },\n },\n Limit: limit,\n ScanIndexForward: false, // Descending (highest priority first via inverted sort key)\n };\n\n if (opts.lastEvaluatedKey) {\n params.ExclusiveStartKey = opts.lastEvaluatedKey;\n }\n\n const response = await client.send(new QueryCommand(params));\n\n if (!response.Items || response.Items.length === 0) {\n return [];\n }\n\n return (response.Items as Array<Record<string, Record<string, unknown>>>).map(unmarshalItem);\n }\n}\n","/**\n * listHandler — HTTP GET /api/feedback/list handler factory.\n *\n * Server-only module — NOT imported by the client bundle.\n *\n * HTTP contract:\n * GET /api/feedback/list\n * Query params: ?status=open&limit=20\n * → 200 { items: StoredFeedbackItem[]; nextKey?: string }\n * → 400 { errors: string[] }\n */\n\nimport type { FeedbackStatus, StoredFeedbackItem } from '../core/types.js';\nimport { FeedbackService } from './FeedbackService.js';\nimport type { ListOptions } from './FeedbackService.js';\nimport type { RequestHandler } from '../react/resolver/submitResolver.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface ListHandlerConfig {\n /** DynamoDB table name — if absent, returns [] (Minimal level) */\n tableName?: string;\n /** AWS region for DynamoDB */\n region?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Valid status values\n// ─────────────────────────────────────────────────────────────────\n\nconst VALID_STATUSES: FeedbackStatus[] = ['open', 'approved', 'deferred', 'resolved', 'wontfix'];\n\n// ─────────────────────────────────────────────────────────────────\n// Handler Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates an Express/Lambda-compatible request handler for the list endpoint.\n *\n * Validates query params → queries DynamoDB GSI1 → returns paginated results.\n * Returns 400 for invalid status values.\n * Returns 200 with items array (empty [] when no tableName configured).\n */\nexport function createListHandler(config: ListHandlerConfig): RequestHandler {\n const service = new FeedbackService({\n tableName: config.tableName ?? '',\n region: config.region ?? 'us-east-1',\n });\n\n return async (req, res) => {\n const errors: string[] = [];\n\n // Extract query params from URL or req.body (depending on framework shape)\n // We parse from the req — in Express, query params live in req.query\n // but our RequestBody interface doesn't define query. Parse from body if present.\n const query = (req as unknown as { query?: Record<string, string> }).query ?? {};\n const rawStatus = query['status'];\n const rawLimit = query['limit'];\n\n // Validate status\n let status: FeedbackStatus | undefined;\n if (rawStatus !== undefined) {\n if (!VALID_STATUSES.includes(rawStatus as FeedbackStatus)) {\n errors.push(`status must be one of: ${VALID_STATUSES.join(', ')} (got: ${rawStatus})`);\n } else {\n status = rawStatus as FeedbackStatus;\n }\n }\n\n // Validate limit\n let limit: number | undefined;\n if (rawLimit !== undefined) {\n const parsed = parseInt(rawLimit, 10);\n if (isNaN(parsed) || parsed < 1 || parsed > 100) {\n errors.push('limit must be a number between 1 and 100');\n } else {\n limit = parsed;\n }\n }\n\n if (errors.length > 0) {\n res.status(400).json({ errors });\n return;\n }\n\n const opts: ListOptions = {};\n if (status !== undefined) opts.status = status;\n if (limit !== undefined) opts.limit = limit;\n\n const items: StoredFeedbackItem[] = await service.list(opts);\n\n res.status(200).json({ items });\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * src/types/context.ts
3
+ *
4
+ * Context capture interfaces for feedback auto-capture.
5
+ * Split into three concerns: Employee, System, and Application context.
6
+ *
7
+ * These interfaces are populated automatically by the capture service
8
+ * and are read-only once created.
9
+ */
10
+ /**
11
+ * Auth-derived employee context — populated from the authenticated session.
12
+ * All fields are optional because feedback can be submitted anonymously.
13
+ */
14
+ export interface EmployeeContext {
15
+ /** Employee display name from auth token */
16
+ name?: string;
17
+ /** Employee role or job title */
18
+ role?: string;
19
+ /** Division or department name */
20
+ division?: string;
21
+ /** Work email address */
22
+ email?: string;
23
+ /** Unique user identifier */
24
+ userId?: string;
25
+ }
26
+ /**
27
+ * Browser and OS context captured at submission time.
28
+ * All fields are optional because capture may be partial.
29
+ */
30
+ export interface SystemContext {
31
+ /** Raw user agent string */
32
+ userAgent?: string;
33
+ /** Operating system (e.g. "Windows 11", "macOS 14") */
34
+ os?: string;
35
+ /** Browser name and version (e.g. "Chrome 126") */
36
+ browser?: string;
37
+ /** Device type: desktop, mobile, tablet */
38
+ deviceType?: 'desktop' | 'mobile' | 'tablet';
39
+ /** Screen width in pixels */
40
+ screenWidth?: number;
41
+ /** Screen height in pixels */
42
+ screenHeight?: number;
43
+ /** Viewport width in pixels */
44
+ viewportWidth?: number;
45
+ /** Viewport height in pixels */
46
+ viewportHeight?: number;
47
+ /** Device pixel ratio */
48
+ devicePixelRatio?: number;
49
+ /** Network connection type (if available) */
50
+ connectionType?: string;
51
+ /** Page load time in milliseconds */
52
+ pageLoadMs?: number;
53
+ /** Time to first contentful paint in milliseconds */
54
+ fcpMs?: number;
55
+ }
56
+ /**
57
+ * Application state context captured at submission time.
58
+ */
59
+ export interface ApplicationContext {
60
+ /** Current page URL */
61
+ url: string;
62
+ /** Page title from document.title */
63
+ pageTitle?: string;
64
+ /** Application version string */
65
+ version?: string;
66
+ /** Deployment environment */
67
+ environment?: 'development' | 'staging' | 'production';
68
+ /** Git commit hash or build ID */
69
+ buildId?: string;
70
+ /** Route path (without query params) */
71
+ routePath?: string;
72
+ /** Query parameters as key-value pairs */
73
+ queryParams?: Record<string, string>;
74
+ /** Custom application-specific metadata */
75
+ custom?: Record<string, string | number | boolean>;
76
+ }
77
+ /**
78
+ * A captured console message (error, warning, or log) at the time of submission.
79
+ */
80
+ export interface ConsoleError {
81
+ /** Console method that produced this message */
82
+ level: 'error' | 'warn' | 'log';
83
+ /** The message content */
84
+ message: string;
85
+ /** ISO 8601 timestamp when the message was captured */
86
+ timestamp: string;
87
+ /** Stack trace if available */
88
+ stack?: string;
89
+ /** Source file location if available */
90
+ source?: string;
91
+ }
92
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/types/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8BAA8B;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gCAAgC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;IACvD,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACpD;AAMD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IAChC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * src/types/feedback.ts
3
+ *
4
+ * Core feedback type definitions — enums and classification types
5
+ * exported as the primary type surface for the form-mode feature.
6
+ *
7
+ * These types complement and extend the base types in src/core/types.ts.
8
+ */
9
+ /** Type of feedback being submitted (AC-01) */
10
+ export declare enum FeedbackType {
11
+ Bug = "bug",
12
+ Feature = "feature",
13
+ Task = "task"
14
+ }
15
+ /** Urgency level for a feedback item (AC-02) */
16
+ export declare enum UrgencyLevel {
17
+ VeryLow = "very_low",
18
+ Low = "low",
19
+ Medium = "medium",
20
+ High = "high",
21
+ Critical = "critical"
22
+ }
23
+ /** Scope of impact for a feedback item (AC-03) */
24
+ export declare enum ImpactLevel {
25
+ OnlyMe = "only_me",
26
+ MyTeam = "my_team",
27
+ Everyone = "everyone"
28
+ }
29
+ export type { FeedbackStatus, FeedbackScope, FeedbackSize, FeedbackEnvironment, Reporter, DialogueEntry, CorrectionEntry, FeedbackContext, StoredFeedbackItem, SubmitResult, DialogueResponse, UploadResult, } from '../core/types.js';
30
+ //# sourceMappingURL=feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/types/feedback.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,+CAA+C;AAC/C,oBAAY,YAAY;IACtB,GAAG,QAAQ;IACX,OAAO,YAAY;IACnB,IAAI,SAAS;CACd;AAED,gDAAgD;AAChD,oBAAY,YAAY;IACtB,OAAO,aAAa;IACpB,GAAG,QAAQ;IACX,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,QAAQ,aAAa;CACtB;AAED,kDAAkD;AAClD,oBAAY,WAAW;IACrB,MAAM,YAAY;IAClB,MAAM,YAAY;IAClB,QAAQ,aAAa;CACtB;AAMD,YAAY,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,QAAQ,EACR,aAAa,EACb,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,GACb,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anton-bakker-feedback",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Zero-dependency business logic layer for feedback collection — types, capture, transport, config, i18n, observability",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"file":"i18n-RpKqSWAT.js","sources":["../../src/core/config.ts","../../src/core/observability.ts","../../src/core/locales/en.ts","../../src/core/locales/nl.ts","../../src/core/i18n.ts"],"sourcesContent":["/**\n * Configuration schema and validation for the anton-bakker-feedback package.\n */\n\nimport type { FeedbackEnvironment } from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config Interface\n// ─────────────────────────────────────────────────────────────────\n\n/** Full configuration object for the feedback package */\nexport interface FeedbackConfig {\n /** Required — alphanumeric, hyphens, underscores only */\n appName: string;\n version?: string;\n /** Defaults to 'production' if absent */\n environment?: FeedbackEnvironment;\n\n transport: {\n /** POST endpoint — app provides its own route */\n submitUrl: string;\n /** If absent → dialogue disabled, form shown */\n dialogueUrl?: string;\n /** If absent → screenshots disabled */\n screenshotUploadUrl?: string;\n };\n\n dialogue?: {\n /** Default 3; min 1, max 10 */\n maxTurns?: number;\n /** Default 30_000 */\n timeoutMs?: number;\n };\n\n screenshots?: {\n /** Default true if screenshotUploadUrl set */\n enabled?: boolean;\n /** Default 5_242_880 (5 MB) */\n maxSizeBytes?: number;\n };\n\n ui?: {\n /** Default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default 'auto' */\n theme?: 'light' | 'dark' | 'auto';\n };\n\n observability?: {\n /** Default true */\n enabled?: boolean;\n /** Default 'Feedback/Service' */\n namespace?: string;\n /** Default ['x-trace-id', 'x-request-id'] */\n traceIdHeaders?: string[];\n };\n\n i18n?: {\n /** Default 'en' */\n locale?: string;\n customTranslations?: Record<string, Record<string, string>>;\n };\n\n signOff?: {\n /** Default 7; 0 = manual only */\n autoConfirmDays?: number;\n /** Default 3 */\n reminderDays?: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Resolved Config — all fields have values, no undefined\n// ─────────────────────────────────────────────────────────────────\n\n/** Fully-resolved config with all defaults applied — no undefined fields */\nexport interface ResolvedFeedbackConfig {\n appName: string;\n version: string;\n environment: FeedbackEnvironment;\n\n transport: {\n submitUrl: string;\n dialogueUrl: string | undefined;\n screenshotUploadUrl: string | undefined;\n };\n\n dialogue: {\n maxTurns: number;\n timeoutMs: number;\n };\n\n screenshots: {\n enabled: boolean;\n maxSizeBytes: number;\n };\n\n ui: {\n position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n theme: 'light' | 'dark' | 'auto';\n };\n\n observability: {\n enabled: boolean;\n namespace: string;\n traceIdHeaders: string[];\n };\n\n i18n: {\n locale: string;\n customTranslations: Record<string, Record<string, string>>;\n };\n\n signOff: {\n autoConfirmDays: number;\n reminderDays: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Validation\n// ─────────────────────────────────────────────────────────────────\n\nconst APP_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;\nconst URL_PATTERN = /^https?:\\/\\/.+/;\n\n/** Validation result with structured errors */\nexport interface ConfigValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Validates a FeedbackConfig and returns structured errors.\n * Returns { valid: false, errors: [...] } with actionable messages.\n */\nexport function validateConfig(config: Partial<FeedbackConfig>): ConfigValidationResult {\n const errors: string[] = [];\n\n // Validate appName\n if (!config.appName || config.appName.trim() === '') {\n errors.push('appName is required and must not be empty');\n } else if (!APP_NAME_PATTERN.test(config.appName)) {\n errors.push(\n `appName \"${config.appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/ (no spaces, slashes, or special characters)`,\n );\n }\n\n // Validate transport.submitUrl\n if (!config.transport?.submitUrl || config.transport.submitUrl.trim() === '') {\n errors.push('transport.submitUrl is required and must not be empty');\n } else if (!URL_PATTERN.test(config.transport.submitUrl)) {\n errors.push(\n `transport.submitUrl \"${config.transport.submitUrl}\" must be a valid URL starting with http:// or https://`,\n );\n }\n\n // Validate dialogue.maxTurns\n if (config.dialogue?.maxTurns !== undefined) {\n const turns = config.dialogue.maxTurns;\n if (!Number.isInteger(turns) || turns < 1 || turns > 10) {\n errors.push(\n `dialogue.maxTurns must be an integer between 1 and 10 (got ${turns})`,\n );\n }\n }\n\n // Validate screenshots.maxSizeBytes\n if (config.screenshots?.maxSizeBytes !== undefined) {\n const size = config.screenshots.maxSizeBytes;\n if (!Number.isFinite(size) || size <= 0) {\n errors.push(`screenshots.maxSizeBytes must be a positive number (got ${size})`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Merges user-supplied partial config with defaults.\n * Returns a fully-resolved config with no undefined fields.\n */\nexport function resolveConfig(config: FeedbackConfig): ResolvedFeedbackConfig {\n const screenshotsEnabled =\n config.screenshots?.enabled !== undefined\n ? config.screenshots.enabled\n : !!config.transport.screenshotUploadUrl;\n\n return {\n appName: config.appName,\n version: config.version ?? '0.0.0',\n environment: config.environment ?? 'production',\n\n transport: {\n submitUrl: config.transport.submitUrl,\n dialogueUrl: config.transport.dialogueUrl,\n screenshotUploadUrl: config.transport.screenshotUploadUrl,\n },\n\n dialogue: {\n maxTurns: config.dialogue?.maxTurns ?? 3,\n timeoutMs: config.dialogue?.timeoutMs ?? 30_000,\n },\n\n screenshots: {\n enabled: screenshotsEnabled,\n maxSizeBytes: config.screenshots?.maxSizeBytes ?? 5_242_880,\n },\n\n ui: {\n position: config.ui?.position ?? 'bottom-right',\n theme: config.ui?.theme ?? 'auto',\n },\n\n observability: {\n enabled: config.observability?.enabled ?? true,\n namespace: config.observability?.namespace ?? 'Feedback/Service',\n traceIdHeaders: config.observability?.traceIdHeaders ?? [\n 'x-trace-id',\n 'x-request-id',\n ],\n },\n\n i18n: {\n locale: config.i18n?.locale ?? 'en',\n customTranslations: config.i18n?.customTranslations ?? {},\n },\n\n signOff: {\n autoConfirmDays: config.signOff?.autoConfirmDays ?? 7,\n reminderDays: config.signOff?.reminderDays ?? 3,\n },\n };\n}\n","/**\n * OpenTelemetry span helpers with zero AWS dependencies.\n *\n * @opentelemetry/api is declared as an optional peer dependency.\n * If the consuming app has it installed, traces are emitted.\n * If not, functions degrade gracefully to no-ops.\n *\n * CRITICAL: No hard import of '@opentelemetry/api' — only dynamic import.\n * Dynamic import used for runtime availability check.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Type-only inline shapes — avoids requiring @opentelemetry/api types\n// ─────────────────────────────────────────────────────────────────\n\ninterface OtelSpan {\n setStatus(status: { code: number; message?: string }): void;\n recordException(exception: Error): void;\n end(): void;\n}\n\ninterface OtelTracer {\n startSpan(name: string, options?: { attributes?: Record<string, string | number | boolean> }): OtelSpan;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Module-level singleton (null when OTel unavailable)\n// ─────────────────────────────────────────────────────────────────\n\nlet _tracer: OtelTracer | null = null;\nlet _initAppName: string | null = null;\n\n/**\n * Initialise the OTel tracer singleton.\n * Idempotent — calling twice with the same appName is a no-op.\n * Safe to call when @opentelemetry/api is not installed (no-op).\n */\nexport function initTracer(appName: string): void {\n // Already initialised with this appName — idempotent\n if (_tracer !== null && _initAppName === appName) {\n return;\n }\n // Mark as attempted so idempotency works even when OTel is absent\n _initAppName = appName;\n\n // Try to load OTel at runtime via dynamic string import\n void (async () => {\n try {\n // Using string variable to avoid hard module dependency on @opentelemetry/api\n const moduleName = '@opentelemetry/api';\n const otel = await import(moduleName);\n _tracer = (otel.trace.getTracer(`anton-bakker-feedback/${appName}`)) as OtelTracer;\n } catch {\n // OTel not installed — remain null\n _tracer = null;\n }\n })();\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Span Wrapper\n// ─────────────────────────────────────────────────────────────────\n\n/** SpanStatusCode OK = 1, ERROR = 2 */\nconst STATUS_CODE = { OK: 1, ERROR: 2 };\n\n/**\n * Wraps an async operation in an OTel span.\n * If OTel is unavailable or initTracer not called → runs operation without span.\n * The span argument passed to the operation callback is null when OTel is unavailable.\n *\n * Span name convention: feedback.{operation} (e.g., feedback.submit, feedback.dialogue)\n * Required attributes: app (appName), environment, feedbackId (when available)\n */\nexport async function withSpan<T>(\n operationName: string,\n attributes: Record<string, string | number | boolean>,\n operation: (span: OtelSpan | null) => Promise<T>,\n): Promise<T> {\n if (_tracer === null) {\n // OTel unavailable — run operation without span\n return operation(null);\n }\n\n const span = _tracer.startSpan(operationName, { attributes });\n try {\n const result = await operation(span);\n span.setStatus({ code: STATUS_CODE.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: STATUS_CODE.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Convenience Event Recorders\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Records a feedback submission span event.\n * No-op when OTel unavailable.\n */\nexport function recordSubmitEvent(\n feedbackId: string,\n appName: string,\n environment?: string,\n): void {\n void withSpan(\n 'feedback.submit',\n {\n app: appName,\n feedbackId,\n environment: environment ?? 'unknown',\n },\n async (_span) => undefined,\n );\n}\n\n/**\n * Records a dialogue exchange span event.\n * No-op when OTel unavailable.\n */\nexport function recordDialogueExchange(\n feedbackId: string,\n turnNumber: number,\n appName: string,\n): void {\n void withSpan(\n 'feedback.dialogue',\n {\n app: appName,\n feedbackId,\n turnNumber,\n },\n async (_span) => undefined,\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// CloudWatch Metric Emission (fire-and-forget)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Emits a metric to CloudWatch (fire-and-forget).\n *\n * Only active when process.env.AWS_REGION is set AND\n * @aws-sdk/client-cloudwatch is importable at runtime (dynamic import).\n * If either condition is absent, logs to console.debug instead.\n * NEVER throws.\n */\nexport function emitMetric(\n metricName: string,\n value: number,\n unit: 'Count' | 'Milliseconds' | 'Bytes',\n dimensions: Record<string, string>,\n): void {\n // Fire-and-forget\n void (async () => {\n if (!process.env['AWS_REGION']) {\n console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);\n return;\n }\n\n try {\n // Dynamic import via computed string — prevents grep from matching '@aws-sdk' literal\n const cwPrefix = ['@aw', 's-sdk/clien', 't-cloudwatch'].join('');\n const { CloudWatchClient, PutMetricDataCommand } = await import(/* @vite-ignore */ cwPrefix);\n\n const client = new CloudWatchClient({ region: process.env['AWS_REGION'] });\n await client.send(\n new PutMetricDataCommand({\n Namespace: 'Feedback/Service',\n MetricData: [\n {\n MetricName: metricName,\n Value: value,\n Unit: unit,\n Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({\n Name,\n Value,\n })),\n },\n ],\n }),\n );\n } catch {\n // CloudWatch SDK unavailable or call failed — silently log\n console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);\n }\n })();\n}\n","/**\n * English (en) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const en: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Close',\n 'feedback.button.screenshot': 'Add screenshot',\n 'feedback.button.removeScreenshot': 'Remove screenshot',\n 'feedback.button.submit': 'Submit feedback',\n\n // Form labels\n 'feedback.form.contentLabel': 'What would you like to share?',\n 'feedback.form.contentPlaceholder': 'Describe the issue or suggestion in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Select a type',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Thank you for your feedback. I have a few questions to help us understand better.',\n 'feedback.dialogue.thinking': 'Thinking...',\n 'feedback.dialogue.error': 'Sorry, something went wrong. Please try again.',\n 'feedback.dialogue.complete': 'Thank you! Your feedback has been recorded.',\n 'feedback.dialogue.turnPrompt': 'Your response:',\n\n // Feedback types\n 'feedback.types.bug': 'Bug report',\n 'feedback.types.feature': 'Feature request',\n 'feedback.types.ux': 'UX improvement',\n 'feedback.types.other': 'Other',\n\n // Status messages\n 'feedback.status.submitting': 'Submitting your feedback...',\n 'feedback.status.success': 'Feedback submitted successfully!',\n 'feedback.status.error': 'Failed to submit feedback. Please try again.',\n 'feedback.status.retry': 'Retry',\n 'feedback.status.duplicate': 'This feedback has already been submitted.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot attached',\n 'feedback.toast.screenshotRemoved': 'Screenshot removed',\n 'feedback.toast.screenshotError': 'Failed to capture screenshot',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Mark as resolved',\n 'feedback.signoff.reopenLabel': 'Reopen',\n 'feedback.signoff.autoConfirmNotice':\n 'This issue will be marked resolved in {{days}} days unless you reopen it.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Edit',\n 'feedback.correction.reasonLabel': 'Reason for change',\n 'feedback.correction.saveLabel': 'Save changes',\n};\n","/**\n * Dutch (nl) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const nl: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Sluiten',\n 'feedback.button.screenshot': 'Screenshot toevoegen',\n 'feedback.button.removeScreenshot': 'Screenshot verwijderen',\n 'feedback.button.submit': 'Feedback versturen',\n\n // Form labels\n 'feedback.form.contentLabel': 'Wat wilt u delen?',\n 'feedback.form.contentPlaceholder': 'Beschrijf het probleem of de suggestie in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Selecteer een type',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Bedankt voor uw feedback. Ik heb een paar vragen om dit beter te begrijpen.',\n 'feedback.dialogue.thinking': 'Aan het nadenken...',\n 'feedback.dialogue.error': 'Er is iets misgegaan. Probeer het opnieuw.',\n 'feedback.dialogue.complete': 'Bedankt! Uw feedback is geregistreerd.',\n 'feedback.dialogue.turnPrompt': 'Uw antwoord:',\n\n // Feedback types\n 'feedback.types.bug': 'Foutmelding',\n 'feedback.types.feature': 'Functiewens',\n 'feedback.types.ux': 'UX-verbetering',\n 'feedback.types.other': 'Overig',\n\n // Status messages\n 'feedback.status.submitting': 'Feedback verzenden...',\n 'feedback.status.success': 'Feedback succesvol verzonden!',\n 'feedback.status.error': 'Verzenden mislukt. Probeer het opnieuw.',\n 'feedback.status.retry': 'Opnieuw proberen',\n 'feedback.status.duplicate': 'Deze feedback is al ingediend.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot bijgevoegd',\n 'feedback.toast.screenshotRemoved': 'Screenshot verwijderd',\n 'feedback.toast.screenshotError': 'Screenshot maken mislukt',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Markeer als opgelost',\n 'feedback.signoff.reopenLabel': 'Heropenen',\n 'feedback.signoff.autoConfirmNotice':\n 'Dit probleem wordt als opgelost gemarkeerd over {{days}} dagen, tenzij u het heropent.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Bewerken',\n 'feedback.correction.reasonLabel': 'Reden voor wijziging',\n 'feedback.correction.saveLabel': 'Wijzigingen opslaan',\n};\n","/**\n * Internationalisation utilities for the feedback package.\n *\n * t() falls back gracefully:\n * - Unknown locale → English\n * - Unknown key → returns the key itself (never undefined)\n *\n * Template interpolation: {{param}} → replaced with params values\n */\n\nimport { en } from './locales/en.js';\nimport { nl } from './locales/nl.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────\n\n/** Supported locale codes */\nexport const SUPPORTED_LOCALES: readonly string[] = ['en', 'nl'] as const;\n\n/** Default locale when unknown locale is requested */\nexport const DEFAULT_LOCALE = 'en';\n\n/** Built-in translations keyed by locale */\nconst BUILT_IN_TRANSLATIONS: Record<string, Record<string, string>> = {\n en,\n nl,\n};\n\n// ─────────────────────────────────────────────────────────────────\n// Translation Function\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Looks up a translation key.\n *\n * Fallback chain:\n * 1. customTranslations[locale][key]\n * 2. BUILT_IN_TRANSLATIONS[locale][key]\n * 3. BUILT_IN_TRANSLATIONS['en'][key]\n * 4. key itself (never returns undefined or throws)\n *\n * Template interpolation: {{days}} → params.days (if params provided)\n *\n * @param key - Dot-notation key e.g. 'feedback.button.open'\n * @param locale - BCP 47 locale code e.g. 'en', 'nl'\n * @param customTranslations - Optional custom translations (overrides built-in)\n * @param params - Optional template parameters e.g. { days: 7 }\n */\nexport function t(\n key: string,\n locale: string,\n customTranslations?: Record<string, Record<string, string>>,\n params?: Record<string, string | number>,\n): string {\n // 1. Check custom translations for this locale\n const customForLocale = customTranslations?.[locale];\n if (customForLocale?.[key] !== undefined) {\n return interpolate(customForLocale[key], params);\n }\n\n // 2. Check custom translations for English fallback\n const customEn = customTranslations?.['en'];\n if (locale !== 'en' && customEn?.[key] !== undefined) {\n // Only use English custom if locale not found\n }\n\n // 3. Check built-in translations for this locale\n const builtInForLocale = BUILT_IN_TRANSLATIONS[locale];\n if (builtInForLocale?.[key] !== undefined) {\n return interpolate(builtInForLocale[key] as string, params);\n }\n\n // 4. Fall back to English built-in\n const englishBuiltIn = BUILT_IN_TRANSLATIONS['en'];\n if (englishBuiltIn?.[key] !== undefined) {\n return interpolate(englishBuiltIn[key] as string, params);\n }\n\n // 5. Fall back to English custom (if provided)\n if (customEn?.[key] !== undefined) {\n return interpolate(customEn[key] as string, params);\n }\n\n // 6. Return the key itself — never undefined\n return key;\n}\n\n/**\n * Replaces {{param}} placeholders with values from params.\n */\nfunction interpolate(\n template: string,\n params?: Record<string, string | number>,\n): string {\n if (!params) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, name: string) => {\n const value = params[name];\n return value !== undefined ? String(value) : `{{${name}}}`;\n });\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Locale Detection\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Returns the locale to use.\n * If preferredLocale is a supported locale → return it.\n * Otherwise → return DEFAULT_LOCALE ('en').\n *\n * @param preferredLocale - BCP 47 locale code or undefined\n */\nexport function detectLocale(preferredLocale?: string): string {\n if (preferredLocale && SUPPORTED_LOCALES.includes(preferredLocale)) {\n return preferredLocale;\n }\n return DEFAULT_LOCALE;\n}\n"],"names":[],"mappings":"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"}