@trops/dash-core 0.1.166 → 0.1.168

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.
@@ -38770,6 +38770,104 @@ var stringify = function(node, options){
38770
38770
  css$1.parse = parse$3;
38771
38771
  css$1.stringify = stringify;
38772
38772
 
38773
+ /**
38774
+ * themeFromUrlErrors.js
38775
+ *
38776
+ * Typed error classes for the Theme from URL extraction pipeline.
38777
+ * Used across dash-core (controller), dash-electron (IPC handler),
38778
+ * and dash-react (UI error mapping).
38779
+ */
38780
+
38781
+ const ERROR_TYPES = {
38782
+ URL_UNREACHABLE: "URL_UNREACHABLE",
38783
+ URL_TIMEOUT: "URL_TIMEOUT",
38784
+ EXTRACTION_FAILED: "EXTRACTION_FAILED",
38785
+ NO_COLORS_FOUND: "NO_COLORS_FOUND",
38786
+ FAVICON_FETCH_FAILED: "FAVICON_FETCH_FAILED",
38787
+ };
38788
+
38789
+ class ThemeExtractionError extends Error {
38790
+ /**
38791
+ * @param {string} message - Developer-facing error message
38792
+ * @param {Object} options
38793
+ * @param {string} options.type - Machine-readable error type (from ERROR_TYPES)
38794
+ * @param {string} options.userMessage - User-facing message for UI display
38795
+ * @param {Error} [options.cause] - Original error that triggered this one
38796
+ */
38797
+ constructor(message, { type, userMessage, cause } = {}) {
38798
+ super(message);
38799
+ this.name = "ThemeExtractionError";
38800
+ this.type = type;
38801
+ this.userMessage = userMessage || "Something went wrong extracting colors.";
38802
+ this.cause = cause || null;
38803
+ }
38804
+ }
38805
+
38806
+ let UrlUnreachableError$1 = class UrlUnreachableError extends ThemeExtractionError {
38807
+ constructor(message, { cause } = {}) {
38808
+ super(message || "URL is unreachable", {
38809
+ type: ERROR_TYPES.URL_UNREACHABLE,
38810
+ userMessage: "Couldn't reach that URL. Check the address.",
38811
+ cause,
38812
+ });
38813
+ this.name = "UrlUnreachableError";
38814
+ }
38815
+ };
38816
+
38817
+ class UrlTimeoutError extends ThemeExtractionError {
38818
+ constructor(message, { cause } = {}) {
38819
+ super(message || "URL load timed out", {
38820
+ type: ERROR_TYPES.URL_TIMEOUT,
38821
+ userMessage: "The site took too long to load. Try a simpler page.",
38822
+ cause,
38823
+ });
38824
+ this.name = "UrlTimeoutError";
38825
+ }
38826
+ }
38827
+
38828
+ let ExtractionFailedError$1 = class ExtractionFailedError extends ThemeExtractionError {
38829
+ constructor(message, { cause } = {}) {
38830
+ super(message || "Color extraction failed", {
38831
+ type: ERROR_TYPES.EXTRACTION_FAILED,
38832
+ userMessage: "Failed to extract colors from this site.",
38833
+ cause,
38834
+ });
38835
+ this.name = "ExtractionFailedError";
38836
+ }
38837
+ };
38838
+
38839
+ let NoColorsFoundError$1 = class NoColorsFoundError extends ThemeExtractionError {
38840
+ constructor(message, { cause } = {}) {
38841
+ super(message || "No usable colors found", {
38842
+ type: ERROR_TYPES.NO_COLORS_FOUND,
38843
+ userMessage: "No usable colors found. Try a more styled page.",
38844
+ cause,
38845
+ });
38846
+ this.name = "NoColorsFoundError";
38847
+ }
38848
+ };
38849
+
38850
+ class FaviconFetchError extends ThemeExtractionError {
38851
+ constructor(message, { cause } = {}) {
38852
+ super(message || "Favicon fetch failed", {
38853
+ type: ERROR_TYPES.FAVICON_FETCH_FAILED,
38854
+ userMessage: "Couldn't fetch the site's favicon.",
38855
+ cause,
38856
+ });
38857
+ this.name = "FaviconFetchError";
38858
+ }
38859
+ }
38860
+
38861
+ var themeFromUrlErrors$1 = {
38862
+ ERROR_TYPES,
38863
+ ThemeExtractionError,
38864
+ UrlUnreachableError: UrlUnreachableError$1,
38865
+ UrlTimeoutError,
38866
+ ExtractionFailedError: ExtractionFailedError$1,
38867
+ NoColorsFoundError: NoColorsFoundError$1,
38868
+ FaviconFetchError,
38869
+ };
38870
+
38773
38871
  /**
38774
38872
  * themeFromUrlController.js
38775
38873
  *
@@ -38783,6 +38881,11 @@ const { Vibrant } = require$$1$6;
38783
38881
  const https$1 = require$$8;
38784
38882
  const http$2 = require$$3$4;
38785
38883
  const { URL: URL$2 } = require$$4$1;
38884
+ const {
38885
+ UrlUnreachableError,
38886
+ ExtractionFailedError,
38887
+ NoColorsFoundError,
38888
+ } = themeFromUrlErrors$1;
38786
38889
 
38787
38890
  // ─── Color conversion helpers ───────────────────────────────────────────────
38788
38891
 
@@ -38838,6 +38941,9 @@ function parseColor(str) {
38838
38941
  );
38839
38942
  }
38840
38943
 
38944
+ console.warn(
38945
+ `[themeFromUrlController] parseColor: unrecognized color format "${str}"`,
38946
+ );
38841
38947
  return null;
38842
38948
  }
38843
38949
 
@@ -39135,12 +39241,22 @@ function resolveUrl(href, baseUrl) {
39135
39241
 
39136
39242
  /**
39137
39243
  * Fetch a URL and return the response as a Buffer.
39138
- * Follows redirects (up to 5). Times out after 10 seconds.
39244
+ * Follows redirects (up to maxRedirects). Times out after 10 seconds.
39139
39245
  * @param {string} url - Absolute URL to fetch
39246
+ * @param {number} [maxRedirects=5] - Maximum number of redirects to follow
39247
+ * @param {number} [_redirectCount=0] - Internal: current redirect count
39140
39248
  * @returns {Promise<Buffer>}
39141
39249
  */
39142
- function fetchBuffer(url) {
39250
+ function fetchBuffer(url, maxRedirects = 5, _redirectCount = 0) {
39143
39251
  return new Promise((resolve, reject) => {
39252
+ if (_redirectCount >= maxRedirects) {
39253
+ reject(
39254
+ new UrlUnreachableError(
39255
+ `Too many redirects (${_redirectCount}) fetching ${url}`,
39256
+ ),
39257
+ );
39258
+ return;
39259
+ }
39144
39260
  const parsedUrl = new URL$2(url);
39145
39261
  const client = parsedUrl.protocol === "https:" ? https$1 : http$2;
39146
39262
  const request = client.get(
@@ -39155,7 +39271,9 @@ function fetchBuffer(url) {
39155
39271
  ) {
39156
39272
  const redirectUrl = resolveUrl(res.headers.location, url);
39157
39273
  if (redirectUrl) {
39158
- fetchBuffer(redirectUrl).then(resolve).catch(reject);
39274
+ fetchBuffer(redirectUrl, maxRedirects, _redirectCount + 1)
39275
+ .then(resolve)
39276
+ .catch(reject);
39159
39277
  return;
39160
39278
  }
39161
39279
  }
@@ -39273,10 +39391,12 @@ function isBoringColor(hex) {
39273
39391
  * Merge colors from all sources, deduplicate via clustering, and rank.
39274
39392
  * @param {Array<{hex, source, confidence}>} allColors - Colors from all extraction stages
39275
39393
  * @param {number} maxColors - Maximum palette size (default: 6)
39276
- * @returns {Array<{hex, rgb, hsl, confidence, sources}>}
39394
+ * @returns {{ palette: Array<{hex, rgb, hsl, confidence, sources}>, reason: string }}
39277
39395
  */
39278
39396
  function mergeAndRank(allColors, maxColors = 6) {
39279
- if (!allColors || allColors.length === 0) return [];
39397
+ if (!allColors || allColors.length === 0) {
39398
+ return { palette: [], reason: "no_colors_extracted" };
39399
+ }
39280
39400
 
39281
39401
  // Build clusters — group colors within deltaE < 10
39282
39402
  const clusters = [];
@@ -39320,6 +39440,11 @@ function mergeAndRank(allColors, maxColors = 6) {
39320
39440
  const interesting = clusters.filter((c) => !isBoringColor(c.hex));
39321
39441
  const boring = clusters.filter((c) => isBoringColor(c.hex));
39322
39442
 
39443
+ // All colors were filtered as boring
39444
+ if (interesting.length === 0 && boring.length === 0) {
39445
+ return { palette: [], reason: "all_colors_filtered" };
39446
+ }
39447
+
39323
39448
  // Score: confidence * frequency weight * saturation bonus
39324
39449
  const scored = interesting.map((c) => {
39325
39450
  const hsl = rgbToHsl$1(c.rgb);
@@ -39367,7 +39492,11 @@ function mergeAndRank(allColors, maxColors = 6) {
39367
39492
  });
39368
39493
  }
39369
39494
 
39370
- return palette;
39495
+ if (palette.length === 0) {
39496
+ return { palette: [], reason: "all_colors_filtered" };
39497
+ }
39498
+
39499
+ return { palette, reason: "success" };
39371
39500
  }
39372
39501
 
39373
39502
  // ─── Main export ─────────────────────────────────────────────────────────────
@@ -39404,6 +39533,11 @@ async function extractColorsFromUrl({
39404
39533
  `[themeFromUrlController] CSS vars: ${cssVarColors.length} colors`,
39405
39534
  );
39406
39535
 
39536
+ // Null guard on computedStyles
39537
+ if (computedStyles != null && typeof computedStyles !== "object") {
39538
+ throw new ExtractionFailedError("computedStyles must be an object or null");
39539
+ }
39540
+
39407
39541
  const computedColors = extractComputedColors(computedStyles);
39408
39542
  console.log(
39409
39543
  `[themeFromUrlController] Computed styles: ${computedColors.length} colors`,
@@ -39429,17 +39563,32 @@ async function extractColorsFromUrl({
39429
39563
  ...cssVarColors,
39430
39564
  ...computedColors,
39431
39565
  ...faviconColors,
39432
- ];
39566
+ ].filter((c) => {
39567
+ if (!c.hex) {
39568
+ console.warn(
39569
+ `[themeFromUrlController] Skipping color with missing hex from source "${c.source}"`,
39570
+ );
39571
+ return false;
39572
+ }
39573
+ return true;
39574
+ });
39433
39575
  console.log(`[themeFromUrlController] Total raw colors: ${allColors.length}`);
39434
39576
 
39435
- const palette = mergeAndRank(allColors);
39577
+ const { palette, reason } = mergeAndRank(allColors);
39436
39578
  console.log(
39437
- `[themeFromUrlController] Final palette: ${palette.length} colors`,
39579
+ `[themeFromUrlController] Final palette: ${palette.length} colors (reason: ${reason})`,
39438
39580
  );
39439
39581
 
39582
+ if (palette.length === 0) {
39583
+ throw new NoColorsFoundError(
39584
+ `No usable colors extracted (reason: ${reason})`,
39585
+ );
39586
+ }
39587
+
39440
39588
  return {
39441
39589
  palette,
39442
39590
  rawCount: allColors.length,
39591
+ reason,
39443
39592
  };
39444
39593
  }
39445
39594
 
@@ -39727,6 +39876,8 @@ var require$$1 = /*@__PURE__*/getAugmentedNamespace(themeGenerator);
39727
39876
 
39728
39877
  const { TAILWIND_COLORS } = require$$0;
39729
39878
 
39879
+ const VALID_HEX_RE = /^#[0-9a-fA-F]{6}$/;
39880
+
39730
39881
  // ─── Color conversion helpers ───────────────────────────────────────────────
39731
39882
  // These mirror the helpers in themeFromUrlController but are kept local
39732
39883
  // to avoid coupling the two modules.
@@ -39878,8 +40029,38 @@ function assignRoles$1(palette) {
39878
40029
  };
39879
40030
  }
39880
40031
 
40032
+ // Validate and filter palette entries — each must have hex and confidence
40033
+ const validPalette = palette.filter((c) => {
40034
+ if (!c || typeof c !== "object") {
40035
+ console.warn("[paletteToThemeMapper] Skipping non-object palette entry");
40036
+ return false;
40037
+ }
40038
+ if (!c.hex || typeof c.hex !== "string") {
40039
+ console.warn(
40040
+ "[paletteToThemeMapper] Skipping palette entry with missing hex",
40041
+ );
40042
+ return false;
40043
+ }
40044
+ if (c.confidence == null || typeof c.confidence !== "number") {
40045
+ console.warn(
40046
+ `[paletteToThemeMapper] Skipping palette entry "${c.hex}" with missing confidence`,
40047
+ );
40048
+ return false;
40049
+ }
40050
+ return true;
40051
+ });
40052
+
40053
+ if (validPalette.length === 0) {
40054
+ return {
40055
+ primary: "#6b7280",
40056
+ secondary: "#3b82f6",
40057
+ tertiary: "#6366f1",
40058
+ neutral: "#64748b",
40059
+ };
40060
+ }
40061
+
39881
40062
  // Ensure all entries have hsl
39882
- const colors = palette.map((c) => {
40063
+ const colors = validPalette.map((c) => {
39883
40064
  const rgb = c.rgb || parseHex(c.hex);
39884
40065
  const hsl = c.hsl || (rgb ? rgbToHsl(rgb) : { h: 0, s: 0, l: 50 });
39885
40066
  return { ...c, rgb, hsl };
@@ -39982,11 +40163,18 @@ function generateThemeFromPalette$1(palette, overrides = {}) {
39982
40163
  // Step 1: Assign roles
39983
40164
  const roles = assignRoles$1(palette);
39984
40165
 
39985
- // Apply any user overrides
39986
- if (overrides.primary) roles.primary = overrides.primary;
39987
- if (overrides.secondary) roles.secondary = overrides.secondary;
39988
- if (overrides.tertiary) roles.tertiary = overrides.tertiary;
39989
- if (overrides.neutral) roles.neutral = overrides.neutral;
40166
+ // Apply any user overrides (validate hex format)
40167
+ for (const role of ["primary", "secondary", "tertiary", "neutral"]) {
40168
+ if (overrides[role]) {
40169
+ if (VALID_HEX_RE.test(overrides[role])) {
40170
+ roles[role] = overrides[role];
40171
+ } else {
40172
+ console.warn(
40173
+ `[paletteToThemeMapper] Skipping invalid override for ${role}: "${overrides[role]}"`,
40174
+ );
40175
+ }
40176
+ }
40177
+ }
39990
40178
 
39991
40179
  // Step 2: Match each role to nearest Tailwind family
39992
40180
  const primaryMatch = matchTailwindFamily$1(roles.primary);
@@ -48615,6 +48803,9 @@ const paletteToThemeMapper = paletteToThemeMapper_1;
48615
48803
  const webSocketController = webSocketController_1;
48616
48804
  const extractionCacheController = extractionCacheController_1;
48617
48805
 
48806
+ // --- Errors ---
48807
+ const themeFromUrlErrors = themeFromUrlErrors$1;
48808
+
48618
48809
  // --- Utils ---
48619
48810
  const clientCache = requireClientCache();
48620
48811
  // auto-register built-in factories
@@ -48736,6 +48927,9 @@ var electron = {
48736
48927
  clientCache,
48737
48928
  responseCache,
48738
48929
 
48930
+ // Errors
48931
+ themeFromUrlErrors,
48932
+
48739
48933
  // Schema
48740
48934
  dashboardConfigValidator,
48741
48935
  dashboardConfigUtils,