@texturehq/edges 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@texturehq/edges",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "author": "Nicholas Brown <nick@texturehq.com>",
5
5
  "description": "A shared component library for Texture",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "dev": "yarn watch",
35
35
  "watch": "tsup --watch",
36
36
  "build": "yarn tokens:build && tsup && yarn build:post",
37
- "build:post": "postcss src/styles.css -o dist/styles.css && node scripts/copy-assets.js && node scripts/generate-components-manifest.js",
37
+ "build:post": "postcss src/styles.css -o dist/styles.css && node scripts/copy-assets.js && node scripts/generate-components-manifest.js && node scripts/generate-utilities-manifest.js",
38
38
  "tokens:build": "node style-dictionary.config.mjs",
39
39
  "tokens:build:tailwind": "node style-dictionary.config.mjs",
40
40
  "tokens:watch": "nodemon --watch tokens --ext json --exec 'yarn tokens:build'",
@@ -36,11 +36,13 @@ function extractExportsFromIndex(indexContent) {
36
36
  }
37
37
 
38
38
  function findComponentFile(relPath) {
39
- // Components are in directories like Button/Button.tsx or Button/index.ts
39
+ // Components can be in directories like Button/Button.tsx or charts/ChartAxis/ChartAxis.tsx
40
40
  const base = path.join(SRC_DIR, "components", relPath);
41
+ // Get the component name (last part of the path)
42
+ const componentName = relPath.split('/').pop();
41
43
  const candidates = [
42
- path.join(base, `${relPath}.tsx`),
43
- path.join(base, `${relPath}.ts`),
44
+ path.join(base, `${componentName}.tsx`),
45
+ path.join(base, `${componentName}.ts`),
44
46
  path.join(base, `index.tsx`),
45
47
  path.join(base, `index.ts`),
46
48
  ];
@@ -122,12 +124,36 @@ function run() {
122
124
  if (!componentFile) continue;
123
125
  const content = read(componentFile);
124
126
 
125
- // Description: JSDoc above `export function Name` or above `export interface NameProps`
126
- let idx = content.indexOf(`export function ${name}`);
127
- if (idx === -1) idx = content.search(new RegExp(`export\\s+(const|class)\\s+${name}\b`));
128
- if (idx === -1)
129
- idx = content.search(new RegExp(`export\\s+(interface|type)\\s+${name}Props\b`));
130
- const description = idx !== -1 ? getJsdocAbove(content, idx) : "";
127
+ // Description: JSDoc above various export patterns
128
+ let idx = -1;
129
+ let description = "";
130
+
131
+ // Try multiple patterns to find the component export
132
+ const patterns = [
133
+ `export function ${name}`,
134
+ `export const ${name}:`,
135
+ `export const ${name} =`,
136
+ `export class ${name}`,
137
+ new RegExp(`export\\s+const\\s+${name}\\s*[:=]`),
138
+ new RegExp(`export\\s+function\\s+${name}\\s*[(<]`),
139
+ new RegExp(`export\\s+class\\s+${name}\\s*[{<]`),
140
+ new RegExp(`export\\s+(interface|type)\\s+${name}Props\\b`)
141
+ ];
142
+
143
+ for (const pattern of patterns) {
144
+ if (typeof pattern === 'string') {
145
+ idx = content.indexOf(pattern);
146
+ } else {
147
+ const match = content.search(pattern);
148
+ if (match !== -1) idx = match;
149
+ }
150
+ if (idx !== -1) break;
151
+ }
152
+
153
+ // If we found an export, get the JSDoc above it
154
+ if (idx !== -1) {
155
+ description = getJsdocAbove(content, idx);
156
+ }
131
157
 
132
158
  const props = extractProps(content, name);
133
159
 
@@ -0,0 +1,369 @@
1
+ // generate-utilities-manifest.js
2
+ // Build-time script that scans exported utilities and creates dist/utilities.manifest.json
3
+
4
+ import fs from "fs";
5
+ import path from "path";
6
+
7
+ const ROOT = process.cwd();
8
+ const SRC_DIR = path.join(ROOT, "src");
9
+ const DIST_DIR = path.join(ROOT, "dist");
10
+
11
+ function read(file) {
12
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
13
+ }
14
+
15
+ function ensureDir(dir) {
16
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ /**
20
+ * Extract JSDoc comment from text above a function/const
21
+ */
22
+ function extractJSDoc(content, functionName) {
23
+ // Look for JSDoc block immediately before the function/const declaration
24
+ const patterns = [
25
+ // Function declaration: /** ... */ export function name OR export async function name
26
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+(?:async\\s+)?function\\s+${functionName}\\b`, "s"),
27
+ // Arrow function: /** ... */ export const name =
28
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+const\\s+${functionName}\\s*=`, "s"),
29
+ // Type export: /** ... */ export type name
30
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+type\\s+${functionName}\\b`, "s"),
31
+ // Interface export: /** ... */ export interface name
32
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+interface\\s+${functionName}\\b`, "s"),
33
+ ];
34
+
35
+ for (const pattern of patterns) {
36
+ const match = content.match(pattern);
37
+ if (match) {
38
+ const jsdocMatch = match[0].match(/\/\*\*([\s\S]*?)\*\//);
39
+ if (jsdocMatch) {
40
+ // Clean up the JSDoc comment
41
+ const cleaned = jsdocMatch[1]
42
+ .split("\n")
43
+ .map((line) => line.replace(/^\s*\*\s?/, "").trim())
44
+ .filter((line) => line && !line.startsWith("@"))
45
+ .join(" ")
46
+ .trim();
47
+ return cleaned;
48
+ }
49
+ }
50
+ }
51
+ return "";
52
+ }
53
+
54
+ /**
55
+ * Extract @param tags from JSDoc
56
+ */
57
+ function extractParams(content, functionName) {
58
+ const patterns = [
59
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+(?:async\\s+)?function\\s+${functionName}\\b`, "s"),
60
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+const\\s+${functionName}\\s*=`, "s"),
61
+ ];
62
+
63
+ for (const pattern of patterns) {
64
+ const match = content.match(pattern);
65
+ if (match) {
66
+ const jsdocMatch = match[0].match(/\/\*\*([\s\S]*?)\*\//);
67
+ if (jsdocMatch) {
68
+ const params = [];
69
+ const lines = jsdocMatch[1].split("\n");
70
+ for (const line of lines) {
71
+ const paramMatch = line.match(/@param\s+(?:\{([^}]+)\})?\s*(\S+)\s*-?\s*(.*)/);
72
+ if (paramMatch) {
73
+ params.push({
74
+ name: paramMatch[2],
75
+ type: paramMatch[1] || "unknown",
76
+ description: paramMatch[3].trim(),
77
+ });
78
+ }
79
+ }
80
+ return params;
81
+ }
82
+ }
83
+ }
84
+ return [];
85
+ }
86
+
87
+ /**
88
+ * Extract @returns tag from JSDoc
89
+ */
90
+ function extractReturns(content, functionName) {
91
+ const patterns = [
92
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+(?:async\\s+)?function\\s+${functionName}\\b`, "s"),
93
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+const\\s+${functionName}\\s*=`, "s"),
94
+ ];
95
+
96
+ for (const pattern of patterns) {
97
+ const match = content.match(pattern);
98
+ if (match) {
99
+ const jsdocMatch = match[0].match(/\/\*\*([\s\S]*?)\*\//);
100
+ if (jsdocMatch) {
101
+ const lines = jsdocMatch[1].split("\n");
102
+ for (const line of lines) {
103
+ const returnMatch = line.match(/@returns?\s+(?:\{([^}]+)\})?\s*(.*)/);
104
+ if (returnMatch) {
105
+ return {
106
+ type: returnMatch[1] || "unknown",
107
+ description: returnMatch[2].trim(),
108
+ };
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Extract @example from JSDoc
119
+ */
120
+ function extractExample(content, functionName) {
121
+ const patterns = [
122
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+(?:async\\s+)?function\\s+${functionName}\\b`, "s"),
123
+ new RegExp(`\\/\\*\\*([^*]|\\*(?!\\/))*\\*\\/\\s*export\\s+const\\s+${functionName}\\s*=`, "s"),
124
+ ];
125
+
126
+ for (const pattern of patterns) {
127
+ const match = content.match(pattern);
128
+ if (match) {
129
+ const jsdocMatch = match[0].match(/\/\*\*([\s\S]*?)\*\//);
130
+ if (jsdocMatch) {
131
+ const exampleMatch = jsdocMatch[1].match(/@example\s*([\s\S]*?)(?=@\w+|$)/);
132
+ if (exampleMatch) {
133
+ // Clean up the example
134
+ const example = exampleMatch[1]
135
+ .split("\n")
136
+ .map((line) => line.replace(/^\s*\*\s?/, ""))
137
+ .join("\n")
138
+ .trim();
139
+ // Remove ```typescript and ``` markers if present
140
+ return example.replace(/^```\w*\n?/, "").replace(/\n?```$/, "").trim();
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ /**
149
+ * Extract exported functions from a file
150
+ */
151
+ function extractExports(filePath) {
152
+ const content = read(filePath);
153
+ if (!content) return [];
154
+
155
+ const utilities = [];
156
+
157
+ // Match export function declarations
158
+ const functionPattern = /export\s+(?:async\s+)?function\s+(\w+)/g;
159
+ let match;
160
+ while ((match = functionPattern.exec(content))) {
161
+ const name = match[1];
162
+ utilities.push({
163
+ name,
164
+ type: "function",
165
+ description: extractJSDoc(content, name),
166
+ params: extractParams(content, name),
167
+ returns: extractReturns(content, name),
168
+ example: extractExample(content, name),
169
+ });
170
+ }
171
+
172
+ // Match export const arrow functions and constants
173
+ const constPattern = /export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=/g;
174
+ while ((match = constPattern.exec(content))) {
175
+ const name = match[1];
176
+ // Check if it's likely a function (has => or function keyword after =)
177
+ const afterMatch = content.slice(match.index + match[0].length, match.index + match[0].length + 50);
178
+ const isFunction = afterMatch.includes("=>") || afterMatch.includes("function");
179
+
180
+ utilities.push({
181
+ name,
182
+ type: isFunction ? "function" : "constant",
183
+ description: extractJSDoc(content, name),
184
+ params: isFunction ? extractParams(content, name) : [],
185
+ returns: isFunction ? extractReturns(content, name) : null,
186
+ example: extractExample(content, name),
187
+ });
188
+ }
189
+
190
+ // Match export type/interface (for documentation purposes)
191
+ const typePattern = /export\s+(?:type|interface)\s+(\w+)/g;
192
+ while ((match = typePattern.exec(content))) {
193
+ const name = match[1];
194
+ utilities.push({
195
+ name,
196
+ type: "type",
197
+ description: extractJSDoc(content, name),
198
+ example: extractExample(content, name),
199
+ });
200
+ }
201
+
202
+ return utilities;
203
+ }
204
+
205
+ /**
206
+ * Scan a directory for utility files
207
+ */
208
+ function scanDirectory(dir, category) {
209
+ const utilities = [];
210
+
211
+ if (!fs.existsSync(dir)) return utilities;
212
+
213
+ const files = fs.readdirSync(dir);
214
+
215
+ for (const file of files) {
216
+ const filePath = path.join(dir, file);
217
+ const stat = fs.statSync(filePath);
218
+
219
+ if (stat.isDirectory() && file !== "__tests__" && file !== "tests") {
220
+ // Recursively scan subdirectories
221
+ utilities.push(...scanDirectory(filePath, `${category}/${file}`));
222
+ } else if ((file.endsWith(".ts") || file.endsWith(".tsx")) && !file.endsWith(".d.ts")) {
223
+ // Skip test files, stories, and type definition files
224
+ if (
225
+ file.includes(".test.") ||
226
+ file.includes(".spec.") ||
227
+ file.includes(".stories.")
228
+ ) {
229
+ continue;
230
+ }
231
+
232
+ const funcs = extractExports(filePath);
233
+ for (const func of funcs) {
234
+ utilities.push({
235
+ ...func,
236
+ category,
237
+ file: path.relative(SRC_DIR, filePath),
238
+ });
239
+ }
240
+ }
241
+ }
242
+
243
+ return utilities;
244
+ }
245
+
246
+ function run() {
247
+ try {
248
+ console.log("Generating utilities manifest...");
249
+
250
+ const manifest = {
251
+ version: process.env.npm_package_version || "unknown",
252
+ generatedAt: new Date().toISOString(),
253
+ categories: {},
254
+ };
255
+
256
+ // Scan hooks directory
257
+ const hooksDir = path.join(SRC_DIR, "hooks");
258
+ const hooks = scanDirectory(hooksDir, "hooks");
259
+ if (hooks.length > 0) {
260
+ manifest.categories.hooks = {
261
+ description: "React hooks for common functionality like debouncing, local storage, and time controls",
262
+ utilities: hooks.sort((a, b) => a.name.localeCompare(b.name)),
263
+ };
264
+ }
265
+
266
+ // Scan utils directory
267
+ const utilsDir = path.join(SRC_DIR, "utils");
268
+
269
+ // Formatting utilities
270
+ const formattingDir = path.join(utilsDir, "formatting");
271
+ const formatting = scanDirectory(formattingDir, "formatting");
272
+ if (formatting.length > 0) {
273
+ manifest.categories.formatting = {
274
+ description: "Comprehensive formatting utilities for text, numbers, dates, currency, energy, temperature, distance, and more",
275
+ utilities: formatting.sort((a, b) => a.name.localeCompare(b.name)),
276
+ };
277
+ }
278
+
279
+ // Chart utilities
280
+ const chartsDir = path.join(utilsDir, "charts");
281
+ const charts = scanDirectory(chartsDir, "charts");
282
+ const chartExportUtils = extractExports(path.join(utilsDir, "chartExport.ts"));
283
+ const chartUtils = extractExports(path.join(utilsDir, "charts.ts"));
284
+ const allChartUtils = [
285
+ ...charts,
286
+ ...chartExportUtils.map(f => ({ ...f, category: "charts", file: "utils/chartExport.ts" })),
287
+ ...chartUtils.map(f => ({ ...f, category: "charts", file: "utils/charts.ts" }))
288
+ ];
289
+
290
+ if (allChartUtils.length > 0) {
291
+ manifest.categories.charts = {
292
+ description: "Chart utilities for data visualization, scaling, and export functionality",
293
+ utilities: allChartUtils.sort((a, b) => a.name.localeCompare(b.name)),
294
+ };
295
+ }
296
+
297
+ // Color utilities
298
+ const colorUtils = extractExports(path.join(utilsDir, "colors.ts"));
299
+ if (colorUtils.length > 0) {
300
+ manifest.categories.colors = {
301
+ description: "Color management utilities for theme-aware color resolution, contrast calculation, and palette generation",
302
+ utilities: colorUtils.map(f => ({ ...f, category: "colors", file: "utils/colors.ts" }))
303
+ .sort((a, b) => a.name.localeCompare(b.name)),
304
+ };
305
+ }
306
+
307
+ // General utilities from utils/index.ts
308
+ const generalUtils = extractExports(path.join(utilsDir, "index.ts"));
309
+ if (generalUtils.length > 0) {
310
+ manifest.categories.general = {
311
+ description: "General utility functions for focus management, Tailwind class composition, and theme utilities",
312
+ utilities: generalUtils.map(f => ({ ...f, category: "general", file: "utils/index.ts" }))
313
+ .sort((a, b) => a.name.localeCompare(b.name)),
314
+ };
315
+ }
316
+
317
+ // Add summary
318
+ const totalUtilities = Object.values(manifest.categories).reduce(
319
+ (sum, cat) => sum + cat.utilities.length,
320
+ 0
321
+ );
322
+
323
+ manifest.summary = {
324
+ totalUtilities,
325
+ totalCategories: Object.keys(manifest.categories).length,
326
+ categories: Object.keys(manifest.categories).map(cat => ({
327
+ name: cat,
328
+ count: manifest.categories[cat].utilities.length,
329
+ })),
330
+ };
331
+
332
+ // Add import information
333
+ manifest.importInfo = {
334
+ package: "@texturehq/edges",
335
+ examples: [
336
+ {
337
+ description: "Import specific utilities",
338
+ code: 'import { formatNumber, formatCurrency, useDebounce } from "@texturehq/edges"',
339
+ },
340
+ {
341
+ description: "Import all formatting utilities",
342
+ code: 'import * as formatting from "@texturehq/edges"',
343
+ },
344
+ {
345
+ description: "Import hooks",
346
+ code: 'import { useDebounce, useLocalStorage } from "@texturehq/edges"',
347
+ },
348
+ {
349
+ description: "Import color utilities",
350
+ code: 'import { getResolvedColor, isLightColor } from "@texturehq/edges"',
351
+ }
352
+ ],
353
+ };
354
+
355
+ // Write the manifest
356
+ ensureDir(DIST_DIR);
357
+ const outputPath = path.join(DIST_DIR, "utilities.manifest.json");
358
+ fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
359
+
360
+ console.log(
361
+ `Generated ${path.join("dist", "utilities.manifest.json")} with ${totalUtilities} utilities in ${Object.keys(manifest.categories).length} categories.`
362
+ );
363
+ } catch (err) {
364
+ console.error("Failed to generate utilities manifest:", err);
365
+ process.exitCode = 1;
366
+ }
367
+ }
368
+
369
+ run();