@trops/dash-core 0.1.603 → 0.1.605

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/dist/index.esm.js CHANGED
@@ -2549,6 +2549,27 @@ var ThemeWrapper = function ThemeWrapper(_ref) {
2549
2549
  // eslint-disable-next-line react-hooks/exhaustive-deps
2550
2550
  }, [chosenTheme, themeVariant, themeName, themesForApplication, rawThemes]);
2551
2551
 
2552
+ // Broadcast the app-level theme to a SEPARATE global so
2553
+ // components rendered OUTSIDE the React theme tree
2554
+ // (WidgetBuilderModal, AppUpdatesModal — they live as siblings
2555
+ // of DashboardStage in dash-electron's Dash.js render) can fall
2556
+ // back to it when no dashboard-specific override is in play.
2557
+ //
2558
+ // We can't share the same global with DashboardThemeProvider's
2559
+ // per-workspace broadcast because React commits child effects
2560
+ // BEFORE parent effects — ThemeWrapper would always run last
2561
+ // and silently overwrite the workspace theme. Two separate
2562
+ // globals avoid the race: consumers read
2563
+ // `__dashThemeContext` (workspace) first, fall back to
2564
+ // `__dashAppThemeContext` (app-level) when no workspace is open.
2565
+ useEffect(function () {
2566
+ if (typeof window === "undefined") return undefined;
2567
+ if (contextValue !== null && contextValue !== void 0 && contextValue.currentTheme) {
2568
+ window.__dashAppThemeContext = contextValue;
2569
+ window.dispatchEvent(new Event("dash:theme-changed"));
2570
+ }
2571
+ }, [contextValue]);
2572
+
2552
2573
  // Write the active theme's CSS custom properties to :root so any
2553
2574
  // hex-channel tokens (`bg-[var(--primary-700)]` etc. emitted by
2554
2575
  // ThemeModel) resolve app-wide. Without this, saving a hex theme
@@ -5725,6 +5746,45 @@ function ThemeBroadcast(_ref) {
5725
5746
  return null;
5726
5747
  }
5727
5748
 
5749
+ /**
5750
+ * Writes the dashboard theme's cssVars to `:root` while mounted, and
5751
+ * restores the previous values on unmount / theme switch.
5752
+ *
5753
+ * ThemeWrapper writes the APP theme's cssVars at the top of the tree.
5754
+ * Without this helper, a dashboard that overrides the app theme with a
5755
+ * hex-channel theme (like "Slack Generic") would carry the right class
5756
+ * names (`bg-[var(--primary-900)]`) but the `--primary-900` variable
5757
+ * on :root would still be the APP theme's value — or undefined if the
5758
+ * app theme is named-family. Hex-themed surfaces then render with no
5759
+ * background. This effect closes that gap by promoting the dashboard
5760
+ * theme's cssVars to :root for the duration of its mount.
5761
+ */
5762
+ function DashboardCssVarsBridge(_ref2) {
5763
+ var cssVars = _ref2.cssVars;
5764
+ useEffect(function () {
5765
+ if (!cssVars || typeof document === "undefined") return undefined;
5766
+ var root = document.documentElement;
5767
+ var previous = {};
5768
+ var keys = Object.keys(cssVars);
5769
+ for (var _i = 0, _keys = keys; _i < _keys.length; _i++) {
5770
+ var key = _keys[_i];
5771
+ previous[key] = root.style.getPropertyValue(key);
5772
+ root.style.setProperty(key, cssVars[key]);
5773
+ }
5774
+ return function () {
5775
+ for (var _i2 = 0, _keys2 = keys; _i2 < _keys2.length; _i2++) {
5776
+ var _key = _keys2[_i2];
5777
+ if (previous[_key]) {
5778
+ root.style.setProperty(_key, previous[_key]);
5779
+ } else {
5780
+ root.style.removeProperty(_key);
5781
+ }
5782
+ }
5783
+ };
5784
+ }, [cssVars]);
5785
+ return null;
5786
+ }
5787
+
5728
5788
  /**
5729
5789
  * DashboardThemeProvider
5730
5790
  *
@@ -5735,9 +5795,10 @@ function ThemeBroadcast(_ref) {
5735
5795
  * App chrome (navbar, tab bar, sidebar) stays OUTSIDE this wrapper
5736
5796
  * and keeps the app theme.
5737
5797
  */
5738
- var DashboardThemeProvider = function DashboardThemeProvider(_ref2) {
5739
- var themeKey = _ref2.themeKey,
5740
- children = _ref2.children;
5798
+ var DashboardThemeProvider = function DashboardThemeProvider(_ref3) {
5799
+ var _contextValue$current;
5800
+ var themeKey = _ref3.themeKey,
5801
+ children = _ref3.children;
5741
5802
  var parentContext = useContext(ThemeContext);
5742
5803
  var themes = parentContext.themes,
5743
5804
  themeVariant = parentContext.themeVariant;
@@ -5769,6 +5830,8 @@ var DashboardThemeProvider = function DashboardThemeProvider(_ref2) {
5769
5830
  value: contextValue,
5770
5831
  children: [/*#__PURE__*/jsx(ThemeBroadcast, {
5771
5832
  ctx: contextValue
5833
+ }), /*#__PURE__*/jsx(DashboardCssVarsBridge, {
5834
+ cssVars: contextValue === null || contextValue === void 0 || (_contextValue$current = contextValue.currentTheme) === null || _contextValue$current === void 0 ? void 0 : _contextValue$current.cssVars
5772
5835
  }), children]
5773
5836
  });
5774
5837
  };
@@ -28958,6 +29021,36 @@ var WorkspaceModel = function WorkspaceModel(workspaceItem) {
28958
29021
  function ownKeys$H(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
28959
29022
  function _objectSpread$H(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$H(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$H(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
28960
29023
 
29024
+ /**
29025
+ * Last-resort family for ANY channel whose value can't be resolved
29026
+ * (unknown name, missing palette entry, missing shade, …). Picked
29027
+ * because it's universally safelist-covered, has every shade from
29028
+ * 50..950, and is visually neutral — so a misconfigured theme
29029
+ * renders "muted" rather than blank/transparent.
29030
+ *
29031
+ * Slack Generic etc. exposed this: when a channel value was missing
29032
+ * or unrecognized, ThemeModel silently emitted `bg-undefined-700`
29033
+ * (not in any safelist) → Tailwind dropped the class → the
29034
+ * dropdown popover rendered transparent. Falling back here means
29035
+ * no token can be "missing" by the time `getStylesForItem` reads it.
29036
+ */
29037
+ var FALLBACK_FAMILY = "gray";
29038
+
29039
+ /**
29040
+ * Resolve a channel value to a safelist-covered Tailwind family
29041
+ * name. Returns the original value when it's a hex (the caller
29042
+ * handles those via CSS variables) or a recognized palette family;
29043
+ * otherwise returns FALLBACK_FAMILY so the resulting class always
29044
+ * renders.
29045
+ */
29046
+ function resolveChannelFamily(channelValue) {
29047
+ if (isHexColor$1(channelValue)) return channelValue;
29048
+ if (typeof channelValue === "string" && channelValue.length > 0 && TAILWIND_PALETTE && TAILWIND_PALETTE[channelValue]) {
29049
+ return channelValue;
29050
+ }
29051
+ return FALLBACK_FAMILY;
29052
+ }
29053
+
28961
29054
  /**
28962
29055
  * getNextLevel
28963
29056
  * Need to generate the levels for tailwind
@@ -28976,6 +29069,11 @@ function invert(shade) {
28976
29069
  * shade. Routes to the arbitrary-value syntax (`bg-[var(--type-shade)]`)
28977
29070
  * when the channel's value is a hex.
28978
29071
  *
29072
+ * Layer 1 fallback (audit follow-up to the Slack Generic bug): if
29073
+ * `channelValue` isn't a hex AND isn't a recognized Tailwind palette
29074
+ * family, fall back to FALLBACK_FAMILY so the emitted class is
29075
+ * always safelist-covered and never resolves to `bg-undefined-700`.
29076
+ *
28979
29077
  * @param {"bg"|"text"|"border"} prefix
28980
29078
  * @param {string} type channel name (primary | secondary | …)
28981
29079
  * @param {number} shade tailwind shade (100..950)
@@ -28985,10 +29083,11 @@ function invert(shade) {
28985
29083
  function classFor(prefix, type, shade, channelValue) {
28986
29084
  var hover = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
28987
29085
  var h = hover ? "hover:" : "";
28988
- if (isHexColor$1(channelValue)) {
29086
+ var resolved = resolveChannelFamily(channelValue);
29087
+ if (isHexColor$1(resolved)) {
28989
29088
  return "".concat(h).concat(prefix, "-[var(--").concat(type, "-").concat(shade, ")]");
28990
29089
  }
28991
- return "".concat(h).concat(prefix, "-").concat(channelValue, "-").concat(shade);
29090
+ return "".concat(h).concat(prefix, "-").concat(resolved, "-").concat(shade);
28992
29091
  }
28993
29092
 
28994
29093
  /**
@@ -29001,8 +29100,10 @@ function classFor(prefix, type, shade, channelValue) {
29001
29100
  * produce a `var(--{type}-{shade})` reference that resolves against
29002
29101
  * the CSS custom properties ThemePreviewProvider writes to :root.
29003
29102
  *
29004
- * Returns null for unknown named-color families or shades caller
29005
- * falls back to className-only reactivity for that token.
29103
+ * Layer 1 fallback: when the channel value or shade can't be
29104
+ * resolved, fall back to FALLBACK_FAMILY's shade so cssValue is
29105
+ * never null. Pairs with classFor's same fallback to guarantee
29106
+ * every (type, shade) tuple yields a renderable token.
29006
29107
  */
29007
29108
  function cssValueFor(type, shade, channelValue) {
29008
29109
  if (isHexColor$1(channelValue)) {
@@ -29015,12 +29116,17 @@ function cssValueFor(type, shade, channelValue) {
29015
29116
  var _hex = shades[shade] || shades[String(shade)];
29016
29117
  if (_hex) return _hex;
29017
29118
  }
29018
- return null;
29119
+ return fallbackCssValue(shade);
29019
29120
  }
29020
29121
  var family = TAILWIND_PALETTE && TAILWIND_PALETTE[channelValue];
29021
- if (!family) return null;
29122
+ if (!family) return fallbackCssValue(shade);
29022
29123
  var hex = family[shade] || family[String(shade)];
29023
- return hex || null;
29124
+ return hex || fallbackCssValue(shade);
29125
+ }
29126
+ function fallbackCssValue(shade) {
29127
+ var fallback = TAILWIND_PALETTE && TAILWIND_PALETTE[FALLBACK_FAMILY];
29128
+ if (!fallback) return null;
29129
+ return fallback[shade] || fallback[String(shade)] || null;
29024
29130
  }
29025
29131
  function gradientFor(direction, type, fromShade, viaShade, toShade, channelValue) {
29026
29132
  if (isHexColor$1(channelValue)) {
@@ -35205,11 +35311,25 @@ var ChannelEditorModal = function ChannelEditorModal(_ref) {
35205
35311
  var nearest = useMemo(function () {
35206
35312
  return nearestSwatch(selectedHex, families);
35207
35313
  }, [selectedHex, families]);
35314
+
35315
+ // Auto-switch the family tab when the underlying selected color
35316
+ // changes (user picked a different channel/shade or the theme
35317
+ // mutated). Track the last hex we synced for so we don't fight
35318
+ // the user's manual tab clicks — without this guard, every
35319
+ // re-render rebuilds `nearest` as a new object reference and the
35320
+ // effect snaps activeFamily back to the family containing the
35321
+ // current color, making it impossible to browse other families.
35322
+ var lastSyncedHexRef = useRef(null);
35208
35323
  useEffect(function () {
35209
- if (!isOpen) return;
35324
+ if (!isOpen) {
35325
+ lastSyncedHexRef.current = null;
35326
+ return;
35327
+ }
35328
+ if (lastSyncedHexRef.current === selectedHex) return;
35329
+ lastSyncedHexRef.current = selectedHex;
35210
35330
  if (nearest) setActiveFamily(nearest.family);
35211
35331
  // eslint-disable-next-line react-hooks/exhaustive-deps
35212
- }, [isOpen, nearest]);
35332
+ }, [isOpen, selectedHex, nearest]);
35213
35333
  function expandChannel(channelKey) {
35214
35334
  setActiveChannel(channelKey);
35215
35335
  setActiveSlot("base");
@@ -54892,13 +55012,41 @@ var DashboardLoaderModal = function DashboardLoaderModal(_ref) {
54892
55012
  var KITCHEN_SINK_PACKAGE = "trops/kitchen-sink";
54893
55013
  var STATE = {
54894
55014
  WELCOME: "welcome",
55015
+ // AUTH_REQUIRED replaces the generic "Install Failed" screen when
55016
+ // the registry returns `authRequired: true`. Surfaces a
55017
+ // benefits-driven sign-in CTA instead of asking the user to
55018
+ // figure out why install died.
55019
+ AUTH_REQUIRED: "auth-required",
54895
55020
  INSTALLING: "installing",
54896
55021
  DONE: "done",
54897
55022
  ERROR: "error"
54898
55023
  };
55024
+
55025
+ /**
55026
+ * Returns the first workspace that was installed from the Kitchen
55027
+ * Sink package, or null. Used for the modal's dedupe check so we
55028
+ * never create a second Kitchen Sink when one already exists.
55029
+ *
55030
+ * Workspaces installed via the registry carry a
55031
+ * `_dashboardConfig.registryPackage` field — historically the
55032
+ * unscoped name ("kitchen-sink"), more recently the scoped form
55033
+ * ("trops/kitchen-sink"). Match on either trailing segment.
55034
+ */
55035
+ function findExistingKitchenSink(workspaces) {
55036
+ if (!Array.isArray(workspaces)) return null;
55037
+ return workspaces.find(function (ws) {
55038
+ var _ws$_dashboardConfig;
55039
+ var pkg = ws === null || ws === void 0 || (_ws$_dashboardConfig = ws._dashboardConfig) === null || _ws$_dashboardConfig === void 0 ? void 0 : _ws$_dashboardConfig.registryPackage;
55040
+ if (typeof pkg !== "string") return false;
55041
+ var trailing = pkg.includes("/") ? pkg.split("/").pop() : pkg;
55042
+ return trailing === "kitchen-sink";
55043
+ }) || null;
55044
+ }
54899
55045
  var OnboardingModal = function OnboardingModal(_ref) {
54900
55046
  var open = _ref.open,
54901
55047
  appId = _ref.appId,
55048
+ _ref$workspaces = _ref.workspaces,
55049
+ workspaces = _ref$workspaces === void 0 ? [] : _ref$workspaces,
54902
55050
  onOpenDashboard = _ref.onOpenDashboard,
54903
55051
  onDismiss = _ref.onDismiss,
54904
55052
  onComplete = _ref.onComplete;
@@ -54918,6 +55066,16 @@ var OnboardingModal = function OnboardingModal(_ref) {
54918
55066
  setInstallError = _useState6[1];
54919
55067
  var installResultRef = useRef(null);
54920
55068
  var cleanupProgressRef = useRef(null);
55069
+
55070
+ // Reused device-code OAuth state machine (same hook AppUpdatesModal
55071
+ // uses). `authFlow` carries the user code + verification URL once
55072
+ // initiateAuth fires; while non-null we render the polling panel.
55073
+ var _useRegistryAuth = useRegistryAuth(),
55074
+ isAuthenticating = _useRegistryAuth.isAuthenticating,
55075
+ authFlow = _useRegistryAuth.authFlow,
55076
+ authError = _useRegistryAuth.authError,
55077
+ initiateAuth = _useRegistryAuth.initiateAuth,
55078
+ cancelAuth = _useRegistryAuth.cancelAuth;
54921
55079
  useEffect(function () {
54922
55080
  if (open) {
54923
55081
  setState(STATE.WELCOME);
@@ -54979,7 +55137,7 @@ var OnboardingModal = function OnboardingModal(_ref) {
54979
55137
  })), [markCompletedAndClose, onDismiss]);
54980
55138
  var handleInstall = useCallback(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
54981
55139
  var _window$mainApi2, _window$mainApi2$onIn;
54982
- var _window$mainApi3, _window$mainApi3$inst, result, _t2;
55140
+ var existing, _window$mainApi3, _window$mainApi3$inst, result, _t2;
54983
55141
  return _regeneratorRuntime.wrap(function (_context3) {
54984
55142
  while (1) switch (_context3.prev = _context3.next) {
54985
55143
  case 0:
@@ -54991,6 +55149,24 @@ var OnboardingModal = function OnboardingModal(_ref) {
54991
55149
  setState(STATE.ERROR);
54992
55150
  return _context3.abrupt("return");
54993
55151
  case 1:
55152
+ // Dedupe: if the user already has a Kitchen Sink workspace
55153
+ // installed from the registry, skip the install entirely and
55154
+ // route them to the existing one. Without this, the modal
55155
+ // could be re-triggered (e.g. after clearing the
55156
+ // onboarding.completed flag) and would silently create
55157
+ // "Kitchen Sink 2", "Kitchen Sink 3", etc.
55158
+ existing = findExistingKitchenSink(workspaces);
55159
+ if (!existing) {
55160
+ _context3.next = 2;
55161
+ break;
55162
+ }
55163
+ installResultRef.current = {
55164
+ success: true,
55165
+ workspace: existing
55166
+ };
55167
+ setState(STATE.DONE);
55168
+ return _context3.abrupt("return");
55169
+ case 2:
54994
55170
  setState(STATE.INSTALLING);
54995
55171
  setProgressItems([]);
54996
55172
  setInstallError(null);
@@ -55018,42 +55194,49 @@ var OnboardingModal = function OnboardingModal(_ref) {
55018
55194
  return next;
55019
55195
  });
55020
55196
  });
55021
- _context3.prev = 2;
55022
- _context3.next = 3;
55197
+ _context3.prev = 3;
55198
+ _context3.next = 4;
55023
55199
  return (_window$mainApi3 = window.mainApi) === null || _window$mainApi3 === void 0 || (_window$mainApi3 = _window$mainApi3.dashboardConfig) === null || _window$mainApi3 === void 0 || (_window$mainApi3$inst = _window$mainApi3.installDashboardFromRegistry) === null || _window$mainApi3$inst === void 0 ? void 0 : _window$mainApi3$inst.call(_window$mainApi3, appId, KITCHEN_SINK_PACKAGE, {});
55024
- case 3:
55200
+ case 4:
55025
55201
  result = _context3.sent;
55026
55202
  if (cleanupProgressRef.current) {
55027
55203
  cleanupProgressRef.current();
55028
55204
  cleanupProgressRef.current = null;
55029
55205
  }
55030
55206
  if (!(!result || !result.success)) {
55031
- _context3.next = 4;
55207
+ _context3.next = 6;
55208
+ break;
55209
+ }
55210
+ if (!(result !== null && result !== void 0 && result.authRequired)) {
55211
+ _context3.next = 5;
55032
55212
  break;
55033
55213
  }
55214
+ setState(STATE.AUTH_REQUIRED);
55215
+ return _context3.abrupt("return");
55216
+ case 5:
55034
55217
  setInstallError((result === null || result === void 0 ? void 0 : result.error) || "Failed to install Kitchen Sink. Check your internet connection and try again.");
55035
55218
  setState(STATE.ERROR);
55036
55219
  return _context3.abrupt("return");
55037
- case 4:
55220
+ case 6:
55038
55221
  installResultRef.current = result;
55039
55222
  setState(STATE.DONE);
55040
- _context3.next = 6;
55223
+ _context3.next = 8;
55041
55224
  break;
55042
- case 5:
55043
- _context3.prev = 5;
55044
- _t2 = _context3["catch"](2);
55225
+ case 7:
55226
+ _context3.prev = 7;
55227
+ _t2 = _context3["catch"](3);
55045
55228
  if (cleanupProgressRef.current) {
55046
55229
  cleanupProgressRef.current();
55047
55230
  cleanupProgressRef.current = null;
55048
55231
  }
55049
55232
  setInstallError((_t2 === null || _t2 === void 0 ? void 0 : _t2.message) || "Installation failed.");
55050
55233
  setState(STATE.ERROR);
55051
- case 6:
55234
+ case 8:
55052
55235
  case "end":
55053
55236
  return _context3.stop();
55054
55237
  }
55055
- }, _callee3, null, [[2, 5]]);
55056
- })), [appId]);
55238
+ }, _callee3, null, [[3, 7]]);
55239
+ })), [appId, workspaces]);
55057
55240
  var handleOpen = useCallback(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee4() {
55058
55241
  var _installResultRef$cur;
55059
55242
  var workspace;
@@ -55078,6 +55261,17 @@ var OnboardingModal = function OnboardingModal(_ref) {
55078
55261
  handleInstall();
55079
55262
  }, [handleInstall]);
55080
55263
 
55264
+ // From the AUTH_REQUIRED state: kick off device-code OAuth. The
55265
+ // hook opens the browser, polls in the background, and fires our
55266
+ // onAuthorized callback on success. We auto-retry the install
55267
+ // there so the user lands on the DONE state without a second
55268
+ // click.
55269
+ var handleSignIn = useCallback(function () {
55270
+ initiateAuth(function () {
55271
+ handleInstall();
55272
+ });
55273
+ }, [initiateAuth, handleInstall]);
55274
+
55081
55275
  // The Modal dispatches setIsOpen(false) on Escape / backdrop click.
55082
55276
  // Route any close attempt through handleSkip so the completion flag
55083
55277
  // is always stamped (otherwise Escape would silently re-show the
@@ -55101,6 +55295,15 @@ var OnboardingModal = function OnboardingModal(_ref) {
55101
55295
  onInstall: handleInstall,
55102
55296
  onSkip: handleSkip,
55103
55297
  textMuted: textMuted
55298
+ }), state === STATE.AUTH_REQUIRED && /*#__PURE__*/jsx(AuthRequiredBody, {
55299
+ onSignIn: handleSignIn,
55300
+ onCancelAuth: cancelAuth,
55301
+ onSkip: handleSkip,
55302
+ isAuthenticating: isAuthenticating,
55303
+ authFlow: authFlow,
55304
+ authError: authError,
55305
+ textMuted: textMuted,
55306
+ borderPanel: borderPanel
55104
55307
  }), state === STATE.INSTALLING && /*#__PURE__*/jsx(InstallingBody, {
55105
55308
  items: progressItems,
55106
55309
  borderPanel: borderPanel,
@@ -55130,11 +55333,13 @@ function WelcomeBody(_ref7) {
55130
55333
  }), /*#__PURE__*/jsx(Heading2, {
55131
55334
  title: "Welcome to Dash"
55132
55335
  })]
55133
- }), /*#__PURE__*/jsxs(Paragraph, {
55336
+ }), /*#__PURE__*/jsx(Paragraph, {
55134
55337
  className: "".concat(textMuted, " mb-6"),
55135
- children: ["Get started with the ", /*#__PURE__*/jsx("strong", {
55136
- children: "Kitchen Sink"
55137
- }), " dashboard \u2014 a curated set of widgets that shows what Dash can do. You can swap anything out, or build your own from scratch later."]
55338
+ children: /*#__PURE__*/jsxs("span", {
55339
+ children: ["Get started with the ", /*#__PURE__*/jsx("strong", {
55340
+ children: "Kitchen Sink"
55341
+ }), " dashboard \u2014 a curated set of widgets that shows what Dash can do. You can swap anything out, or build your own from scratch later."]
55342
+ })
55138
55343
  }), /*#__PURE__*/jsxs("div", {
55139
55344
  className: "flex items-center justify-end gap-3 mt-2",
55140
55345
  children: [/*#__PURE__*/jsx(Button, {
@@ -55295,6 +55500,153 @@ function ErrorBody(_ref1) {
55295
55500
  })]
55296
55501
  });
55297
55502
  }
55503
+ function BenefitRow(_ref10) {
55504
+ var icon = _ref10.icon,
55505
+ title = _ref10.title,
55506
+ description = _ref10.description,
55507
+ textMuted = _ref10.textMuted;
55508
+ return /*#__PURE__*/jsxs("div", {
55509
+ className: "flex items-start gap-3",
55510
+ children: [/*#__PURE__*/jsx("div", {
55511
+ className: "flex-shrink-0 w-7 h-7 flex items-center justify-center mt-0.5",
55512
+ children: /*#__PURE__*/jsx(FontAwesomeIcon, {
55513
+ icon: icon,
55514
+ className: "text-base opacity-70"
55515
+ })
55516
+ }), /*#__PURE__*/jsxs("div", {
55517
+ className: "flex flex-col",
55518
+ children: [/*#__PURE__*/jsx("span", {
55519
+ className: "text-sm font-medium",
55520
+ children: title
55521
+ }), /*#__PURE__*/jsx("span", {
55522
+ className: "text-xs ".concat(textMuted),
55523
+ children: description
55524
+ })]
55525
+ })]
55526
+ });
55527
+ }
55528
+ function AuthRequiredBody(_ref11) {
55529
+ var onSignIn = _ref11.onSignIn,
55530
+ onCancelAuth = _ref11.onCancelAuth,
55531
+ onSkip = _ref11.onSkip,
55532
+ isAuthenticating = _ref11.isAuthenticating,
55533
+ authFlow = _ref11.authFlow,
55534
+ authError = _ref11.authError,
55535
+ textMuted = _ref11.textMuted,
55536
+ borderPanel = _ref11.borderPanel;
55537
+ // While the device-code flow is polling, swap the benefits panel
55538
+ // for the user-code + verification-URL display so the user knows
55539
+ // what to enter in the browser tab we just opened.
55540
+ if (isAuthenticating && authFlow) {
55541
+ return /*#__PURE__*/jsxs(Fragment, {
55542
+ children: [/*#__PURE__*/jsxs("div", {
55543
+ className: "flex items-center gap-3 mb-2",
55544
+ children: [/*#__PURE__*/jsx(FontAwesomeIcon, {
55545
+ icon: "circle-notch",
55546
+ className: "text-2xl animate-spin"
55547
+ }), /*#__PURE__*/jsx(Heading2, {
55548
+ title: "Waiting for browser sign-in"
55549
+ })]
55550
+ }), /*#__PURE__*/jsx(Paragraph, {
55551
+ className: "".concat(textMuted, " mb-4"),
55552
+ children: /*#__PURE__*/jsx("span", {
55553
+ children: "We opened a browser tab where you can sign in to the Dash Registry. Enter this code if prompted:"
55554
+ })
55555
+ }), /*#__PURE__*/jsxs("div", {
55556
+ className: "border ".concat(borderPanel, " rounded-md p-4 mb-4 text-center"),
55557
+ "data-testid": "onboarding-auth-user-code",
55558
+ children: [/*#__PURE__*/jsx("div", {
55559
+ className: "text-2xl font-mono tracking-widest",
55560
+ children: authFlow.userCode || "—"
55561
+ }), /*#__PURE__*/jsx("div", {
55562
+ className: "text-xs ".concat(textMuted, " mt-2 break-all"),
55563
+ children: authFlow.verificationUrlComplete || authFlow.verificationUrl || ""
55564
+ })]
55565
+ }), /*#__PURE__*/jsx("div", {
55566
+ className: "flex items-center justify-end gap-3",
55567
+ children: /*#__PURE__*/jsx(Button, {
55568
+ onClick: onCancelAuth,
55569
+ title: "Cancel",
55570
+ textSize: "text-sm",
55571
+ padding: "py-2 px-4",
55572
+ backgroundColor: "bg-gray-700",
55573
+ textColor: "text-gray-300",
55574
+ hoverTextColor: "hover:text-white",
55575
+ hoverBackgroundColor: "hover:bg-gray-600",
55576
+ "data-testid": "onboarding-auth-cancel-button"
55577
+ })
55578
+ })]
55579
+ });
55580
+ }
55581
+ return /*#__PURE__*/jsxs(Fragment, {
55582
+ children: [/*#__PURE__*/jsxs("div", {
55583
+ className: "flex items-center gap-3 mb-2",
55584
+ children: [/*#__PURE__*/jsx(FontAwesomeIcon, {
55585
+ icon: "user-plus",
55586
+ className: "text-2xl"
55587
+ }), /*#__PURE__*/jsx(Heading2, {
55588
+ title: "Sign in to the Dash Registry"
55589
+ })]
55590
+ }), /*#__PURE__*/jsx(Paragraph, {
55591
+ className: "".concat(textMuted, " mb-5"),
55592
+ children: /*#__PURE__*/jsx("span", {
55593
+ children: "A free Dash account unlocks the community ecosystem. Sign in to continue installing Kitchen Sink \u2014 and to use everything else the registry offers."
55594
+ })
55595
+ }), /*#__PURE__*/jsxs("div", {
55596
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6",
55597
+ children: [/*#__PURE__*/jsx(BenefitRow, {
55598
+ icon: "cube",
55599
+ title: "Install widgets",
55600
+ description: "Browse and one-click install community widgets",
55601
+ textMuted: textMuted
55602
+ }), /*#__PURE__*/jsx(BenefitRow, {
55603
+ icon: "table-cells",
55604
+ title: "Discover dashboards",
55605
+ description: "Like Kitchen Sink \u2014 curated starters built by others",
55606
+ textMuted: textMuted
55607
+ }), /*#__PURE__*/jsx(BenefitRow, {
55608
+ icon: "palette",
55609
+ title: "Apply themes",
55610
+ description: "Community-built themes for any taste",
55611
+ textMuted: textMuted
55612
+ }), /*#__PURE__*/jsx(BenefitRow, {
55613
+ icon: "upload",
55614
+ title: "Publish your own",
55615
+ description: "Share your widgets and dashboards with the Dash community",
55616
+ textMuted: textMuted
55617
+ })]
55618
+ }), authError && /*#__PURE__*/jsx(Paragraph, {
55619
+ className: "text-red-400 mb-4",
55620
+ children: /*#__PURE__*/jsx("span", {
55621
+ children: authError
55622
+ })
55623
+ }), /*#__PURE__*/jsxs("div", {
55624
+ className: "flex items-center justify-end gap-3 mt-2",
55625
+ children: [/*#__PURE__*/jsx(Button, {
55626
+ onClick: onSkip,
55627
+ title: "Skip for now",
55628
+ textSize: "text-sm",
55629
+ padding: "py-2 px-4",
55630
+ backgroundColor: "bg-gray-700",
55631
+ textColor: "text-gray-300",
55632
+ hoverTextColor: "hover:text-white",
55633
+ hoverBackgroundColor: "hover:bg-gray-600",
55634
+ "data-testid": "onboarding-auth-skip-button"
55635
+ }), /*#__PURE__*/jsx(Button, {
55636
+ onClick: onSignIn,
55637
+ title: "Sign in to Registry",
55638
+ textSize: "text-sm",
55639
+ padding: "py-2 px-4",
55640
+ backgroundColor: "bg-blue-600",
55641
+ textColor: "text-white",
55642
+ hoverTextColor: "hover:text-white",
55643
+ hoverBackgroundColor: "hover:bg-blue-500",
55644
+ icon: "arrow-up-right-from-square",
55645
+ "data-testid": "onboarding-auth-signin-button"
55646
+ })]
55647
+ })]
55648
+ });
55649
+ }
55298
55650
 
55299
55651
  var DashCommandPalette = function DashCommandPalette(_ref) {
55300
55652
  var isOpen = _ref.isOpen,
@@ -61308,7 +61660,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61308
61660
  onboardingCheckedRef.current = true;
61309
61661
  var cancelled = false;
61310
61662
  _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
61311
- var _window$mainApi, _window$mainApi$getSt, status;
61663
+ var _window$mainApi, _window$mainApi$getSt, status, alreadyHasKitchenSink;
61312
61664
  return _regeneratorRuntime.wrap(function (_context) {
61313
61665
  while (1) switch (_context.prev = _context.next) {
61314
61666
  case 0:
@@ -61323,7 +61675,20 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61323
61675
  }
61324
61676
  return _context.abrupt("return");
61325
61677
  case 2:
61326
- if (status && status.completed === false && workspaceConfig.length === 0) {
61678
+ // Defense-in-depth: even if `workspaceConfig.length === 0`
61679
+ // briefly reads true during a race between `isLoadingWorkspaces`
61680
+ // and the workspace array populating, don't show the modal
61681
+ // if the user already has a Kitchen Sink dashboard installed
61682
+ // from the registry. Same dedupe rule the modal's install
61683
+ // path applies.
61684
+ alreadyHasKitchenSink = workspaceConfig.some(function (ws) {
61685
+ var _ws$_dashboardConfig;
61686
+ var pkg = ws === null || ws === void 0 || (_ws$_dashboardConfig = ws._dashboardConfig) === null || _ws$_dashboardConfig === void 0 ? void 0 : _ws$_dashboardConfig.registryPackage;
61687
+ if (typeof pkg !== "string") return false;
61688
+ var trailing = pkg.includes("/") ? pkg.split("/").pop() : pkg;
61689
+ return trailing === "kitchen-sink";
61690
+ });
61691
+ if (status && status.completed === false && workspaceConfig.length === 0 && !alreadyHasKitchenSink) {
61327
61692
  setIsOnboardingOpen(true);
61328
61693
  } else {
61329
61694
  setIsOnboardingOpen(false);
@@ -63127,6 +63492,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
63127
63492
  }), /*#__PURE__*/jsx(OnboardingModal, {
63128
63493
  open: isOnboardingOpen === true,
63129
63494
  appId: credentials === null || credentials === void 0 ? void 0 : credentials.appId,
63495
+ workspaces: workspaceConfig,
63130
63496
  onDismiss: function onDismiss() {
63131
63497
  return setIsOnboardingOpen(false);
63132
63498
  },