@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.js CHANGED
@@ -2567,6 +2567,27 @@ var ThemeWrapper = function ThemeWrapper(_ref) {
2567
2567
  // eslint-disable-next-line react-hooks/exhaustive-deps
2568
2568
  }, [chosenTheme, themeVariant, themeName, themesForApplication, rawThemes]);
2569
2569
 
2570
+ // Broadcast the app-level theme to a SEPARATE global so
2571
+ // components rendered OUTSIDE the React theme tree
2572
+ // (WidgetBuilderModal, AppUpdatesModal — they live as siblings
2573
+ // of DashboardStage in dash-electron's Dash.js render) can fall
2574
+ // back to it when no dashboard-specific override is in play.
2575
+ //
2576
+ // We can't share the same global with DashboardThemeProvider's
2577
+ // per-workspace broadcast because React commits child effects
2578
+ // BEFORE parent effects — ThemeWrapper would always run last
2579
+ // and silently overwrite the workspace theme. Two separate
2580
+ // globals avoid the race: consumers read
2581
+ // `__dashThemeContext` (workspace) first, fall back to
2582
+ // `__dashAppThemeContext` (app-level) when no workspace is open.
2583
+ React.useEffect(function () {
2584
+ if (typeof window === "undefined") return undefined;
2585
+ if (contextValue !== null && contextValue !== void 0 && contextValue.currentTheme) {
2586
+ window.__dashAppThemeContext = contextValue;
2587
+ window.dispatchEvent(new Event("dash:theme-changed"));
2588
+ }
2589
+ }, [contextValue]);
2590
+
2570
2591
  // Write the active theme's CSS custom properties to :root so any
2571
2592
  // hex-channel tokens (`bg-[var(--primary-700)]` etc. emitted by
2572
2593
  // ThemeModel) resolve app-wide. Without this, saving a hex theme
@@ -5743,6 +5764,45 @@ function ThemeBroadcast(_ref) {
5743
5764
  return null;
5744
5765
  }
5745
5766
 
5767
+ /**
5768
+ * Writes the dashboard theme's cssVars to `:root` while mounted, and
5769
+ * restores the previous values on unmount / theme switch.
5770
+ *
5771
+ * ThemeWrapper writes the APP theme's cssVars at the top of the tree.
5772
+ * Without this helper, a dashboard that overrides the app theme with a
5773
+ * hex-channel theme (like "Slack Generic") would carry the right class
5774
+ * names (`bg-[var(--primary-900)]`) but the `--primary-900` variable
5775
+ * on :root would still be the APP theme's value — or undefined if the
5776
+ * app theme is named-family. Hex-themed surfaces then render with no
5777
+ * background. This effect closes that gap by promoting the dashboard
5778
+ * theme's cssVars to :root for the duration of its mount.
5779
+ */
5780
+ function DashboardCssVarsBridge(_ref2) {
5781
+ var cssVars = _ref2.cssVars;
5782
+ React.useEffect(function () {
5783
+ if (!cssVars || typeof document === "undefined") return undefined;
5784
+ var root = document.documentElement;
5785
+ var previous = {};
5786
+ var keys = Object.keys(cssVars);
5787
+ for (var _i = 0, _keys = keys; _i < _keys.length; _i++) {
5788
+ var key = _keys[_i];
5789
+ previous[key] = root.style.getPropertyValue(key);
5790
+ root.style.setProperty(key, cssVars[key]);
5791
+ }
5792
+ return function () {
5793
+ for (var _i2 = 0, _keys2 = keys; _i2 < _keys2.length; _i2++) {
5794
+ var _key = _keys2[_i2];
5795
+ if (previous[_key]) {
5796
+ root.style.setProperty(_key, previous[_key]);
5797
+ } else {
5798
+ root.style.removeProperty(_key);
5799
+ }
5800
+ }
5801
+ };
5802
+ }, [cssVars]);
5803
+ return null;
5804
+ }
5805
+
5746
5806
  /**
5747
5807
  * DashboardThemeProvider
5748
5808
  *
@@ -5753,9 +5813,10 @@ function ThemeBroadcast(_ref) {
5753
5813
  * App chrome (navbar, tab bar, sidebar) stays OUTSIDE this wrapper
5754
5814
  * and keeps the app theme.
5755
5815
  */
5756
- var DashboardThemeProvider = function DashboardThemeProvider(_ref2) {
5757
- var themeKey = _ref2.themeKey,
5758
- children = _ref2.children;
5816
+ var DashboardThemeProvider = function DashboardThemeProvider(_ref3) {
5817
+ var _contextValue$current;
5818
+ var themeKey = _ref3.themeKey,
5819
+ children = _ref3.children;
5759
5820
  var parentContext = React.useContext(DashReact.ThemeContext);
5760
5821
  var themes = parentContext.themes,
5761
5822
  themeVariant = parentContext.themeVariant;
@@ -5787,6 +5848,8 @@ var DashboardThemeProvider = function DashboardThemeProvider(_ref2) {
5787
5848
  value: contextValue,
5788
5849
  children: [/*#__PURE__*/jsxRuntime.jsx(ThemeBroadcast, {
5789
5850
  ctx: contextValue
5851
+ }), /*#__PURE__*/jsxRuntime.jsx(DashboardCssVarsBridge, {
5852
+ cssVars: contextValue === null || contextValue === void 0 || (_contextValue$current = contextValue.currentTheme) === null || _contextValue$current === void 0 ? void 0 : _contextValue$current.cssVars
5790
5853
  }), children]
5791
5854
  });
5792
5855
  };
@@ -28976,6 +29039,36 @@ var WorkspaceModel = function WorkspaceModel(workspaceItem) {
28976
29039
  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; }
28977
29040
  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; }
28978
29041
 
29042
+ /**
29043
+ * Last-resort family for ANY channel whose value can't be resolved
29044
+ * (unknown name, missing palette entry, missing shade, …). Picked
29045
+ * because it's universally safelist-covered, has every shade from
29046
+ * 50..950, and is visually neutral — so a misconfigured theme
29047
+ * renders "muted" rather than blank/transparent.
29048
+ *
29049
+ * Slack Generic etc. exposed this: when a channel value was missing
29050
+ * or unrecognized, ThemeModel silently emitted `bg-undefined-700`
29051
+ * (not in any safelist) → Tailwind dropped the class → the
29052
+ * dropdown popover rendered transparent. Falling back here means
29053
+ * no token can be "missing" by the time `getStylesForItem` reads it.
29054
+ */
29055
+ var FALLBACK_FAMILY = "gray";
29056
+
29057
+ /**
29058
+ * Resolve a channel value to a safelist-covered Tailwind family
29059
+ * name. Returns the original value when it's a hex (the caller
29060
+ * handles those via CSS variables) or a recognized palette family;
29061
+ * otherwise returns FALLBACK_FAMILY so the resulting class always
29062
+ * renders.
29063
+ */
29064
+ function resolveChannelFamily(channelValue) {
29065
+ if (DashReact.isHexColor(channelValue)) return channelValue;
29066
+ if (typeof channelValue === "string" && channelValue.length > 0 && DashReact.TAILWIND_PALETTE && DashReact.TAILWIND_PALETTE[channelValue]) {
29067
+ return channelValue;
29068
+ }
29069
+ return FALLBACK_FAMILY;
29070
+ }
29071
+
28979
29072
  /**
28980
29073
  * getNextLevel
28981
29074
  * Need to generate the levels for tailwind
@@ -28994,6 +29087,11 @@ function invert(shade) {
28994
29087
  * shade. Routes to the arbitrary-value syntax (`bg-[var(--type-shade)]`)
28995
29088
  * when the channel's value is a hex.
28996
29089
  *
29090
+ * Layer 1 fallback (audit follow-up to the Slack Generic bug): if
29091
+ * `channelValue` isn't a hex AND isn't a recognized Tailwind palette
29092
+ * family, fall back to FALLBACK_FAMILY so the emitted class is
29093
+ * always safelist-covered and never resolves to `bg-undefined-700`.
29094
+ *
28997
29095
  * @param {"bg"|"text"|"border"} prefix
28998
29096
  * @param {string} type channel name (primary | secondary | …)
28999
29097
  * @param {number} shade tailwind shade (100..950)
@@ -29003,10 +29101,11 @@ function invert(shade) {
29003
29101
  function classFor(prefix, type, shade, channelValue) {
29004
29102
  var hover = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
29005
29103
  var h = hover ? "hover:" : "";
29006
- if (DashReact.isHexColor(channelValue)) {
29104
+ var resolved = resolveChannelFamily(channelValue);
29105
+ if (DashReact.isHexColor(resolved)) {
29007
29106
  return "".concat(h).concat(prefix, "-[var(--").concat(type, "-").concat(shade, ")]");
29008
29107
  }
29009
- return "".concat(h).concat(prefix, "-").concat(channelValue, "-").concat(shade);
29108
+ return "".concat(h).concat(prefix, "-").concat(resolved, "-").concat(shade);
29010
29109
  }
29011
29110
 
29012
29111
  /**
@@ -29019,8 +29118,10 @@ function classFor(prefix, type, shade, channelValue) {
29019
29118
  * produce a `var(--{type}-{shade})` reference that resolves against
29020
29119
  * the CSS custom properties ThemePreviewProvider writes to :root.
29021
29120
  *
29022
- * Returns null for unknown named-color families or shades caller
29023
- * falls back to className-only reactivity for that token.
29121
+ * Layer 1 fallback: when the channel value or shade can't be
29122
+ * resolved, fall back to FALLBACK_FAMILY's shade so cssValue is
29123
+ * never null. Pairs with classFor's same fallback to guarantee
29124
+ * every (type, shade) tuple yields a renderable token.
29024
29125
  */
29025
29126
  function cssValueFor(type, shade, channelValue) {
29026
29127
  if (DashReact.isHexColor(channelValue)) {
@@ -29033,12 +29134,17 @@ function cssValueFor(type, shade, channelValue) {
29033
29134
  var _hex = shades[shade] || shades[String(shade)];
29034
29135
  if (_hex) return _hex;
29035
29136
  }
29036
- return null;
29137
+ return fallbackCssValue(shade);
29037
29138
  }
29038
29139
  var family = DashReact.TAILWIND_PALETTE && DashReact.TAILWIND_PALETTE[channelValue];
29039
- if (!family) return null;
29140
+ if (!family) return fallbackCssValue(shade);
29040
29141
  var hex = family[shade] || family[String(shade)];
29041
- return hex || null;
29142
+ return hex || fallbackCssValue(shade);
29143
+ }
29144
+ function fallbackCssValue(shade) {
29145
+ var fallback = DashReact.TAILWIND_PALETTE && DashReact.TAILWIND_PALETTE[FALLBACK_FAMILY];
29146
+ if (!fallback) return null;
29147
+ return fallback[shade] || fallback[String(shade)] || null;
29042
29148
  }
29043
29149
  function gradientFor(direction, type, fromShade, viaShade, toShade, channelValue) {
29044
29150
  if (DashReact.isHexColor(channelValue)) {
@@ -35223,11 +35329,25 @@ var ChannelEditorModal = function ChannelEditorModal(_ref) {
35223
35329
  var nearest = React.useMemo(function () {
35224
35330
  return nearestSwatch(selectedHex, families);
35225
35331
  }, [selectedHex, families]);
35332
+
35333
+ // Auto-switch the family tab when the underlying selected color
35334
+ // changes (user picked a different channel/shade or the theme
35335
+ // mutated). Track the last hex we synced for so we don't fight
35336
+ // the user's manual tab clicks — without this guard, every
35337
+ // re-render rebuilds `nearest` as a new object reference and the
35338
+ // effect snaps activeFamily back to the family containing the
35339
+ // current color, making it impossible to browse other families.
35340
+ var lastSyncedHexRef = React.useRef(null);
35226
35341
  React.useEffect(function () {
35227
- if (!isOpen) return;
35342
+ if (!isOpen) {
35343
+ lastSyncedHexRef.current = null;
35344
+ return;
35345
+ }
35346
+ if (lastSyncedHexRef.current === selectedHex) return;
35347
+ lastSyncedHexRef.current = selectedHex;
35228
35348
  if (nearest) setActiveFamily(nearest.family);
35229
35349
  // eslint-disable-next-line react-hooks/exhaustive-deps
35230
- }, [isOpen, nearest]);
35350
+ }, [isOpen, selectedHex, nearest]);
35231
35351
  function expandChannel(channelKey) {
35232
35352
  setActiveChannel(channelKey);
35233
35353
  setActiveSlot("base");
@@ -54910,13 +55030,41 @@ var DashboardLoaderModal = function DashboardLoaderModal(_ref) {
54910
55030
  var KITCHEN_SINK_PACKAGE = "trops/kitchen-sink";
54911
55031
  var STATE = {
54912
55032
  WELCOME: "welcome",
55033
+ // AUTH_REQUIRED replaces the generic "Install Failed" screen when
55034
+ // the registry returns `authRequired: true`. Surfaces a
55035
+ // benefits-driven sign-in CTA instead of asking the user to
55036
+ // figure out why install died.
55037
+ AUTH_REQUIRED: "auth-required",
54913
55038
  INSTALLING: "installing",
54914
55039
  DONE: "done",
54915
55040
  ERROR: "error"
54916
55041
  };
55042
+
55043
+ /**
55044
+ * Returns the first workspace that was installed from the Kitchen
55045
+ * Sink package, or null. Used for the modal's dedupe check so we
55046
+ * never create a second Kitchen Sink when one already exists.
55047
+ *
55048
+ * Workspaces installed via the registry carry a
55049
+ * `_dashboardConfig.registryPackage` field — historically the
55050
+ * unscoped name ("kitchen-sink"), more recently the scoped form
55051
+ * ("trops/kitchen-sink"). Match on either trailing segment.
55052
+ */
55053
+ function findExistingKitchenSink(workspaces) {
55054
+ if (!Array.isArray(workspaces)) return null;
55055
+ return workspaces.find(function (ws) {
55056
+ var _ws$_dashboardConfig;
55057
+ var pkg = ws === null || ws === void 0 || (_ws$_dashboardConfig = ws._dashboardConfig) === null || _ws$_dashboardConfig === void 0 ? void 0 : _ws$_dashboardConfig.registryPackage;
55058
+ if (typeof pkg !== "string") return false;
55059
+ var trailing = pkg.includes("/") ? pkg.split("/").pop() : pkg;
55060
+ return trailing === "kitchen-sink";
55061
+ }) || null;
55062
+ }
54917
55063
  var OnboardingModal = function OnboardingModal(_ref) {
54918
55064
  var open = _ref.open,
54919
55065
  appId = _ref.appId,
55066
+ _ref$workspaces = _ref.workspaces,
55067
+ workspaces = _ref$workspaces === void 0 ? [] : _ref$workspaces,
54920
55068
  onOpenDashboard = _ref.onOpenDashboard,
54921
55069
  onDismiss = _ref.onDismiss,
54922
55070
  onComplete = _ref.onComplete;
@@ -54936,6 +55084,16 @@ var OnboardingModal = function OnboardingModal(_ref) {
54936
55084
  setInstallError = _useState6[1];
54937
55085
  var installResultRef = React.useRef(null);
54938
55086
  var cleanupProgressRef = React.useRef(null);
55087
+
55088
+ // Reused device-code OAuth state machine (same hook AppUpdatesModal
55089
+ // uses). `authFlow` carries the user code + verification URL once
55090
+ // initiateAuth fires; while non-null we render the polling panel.
55091
+ var _useRegistryAuth = useRegistryAuth(),
55092
+ isAuthenticating = _useRegistryAuth.isAuthenticating,
55093
+ authFlow = _useRegistryAuth.authFlow,
55094
+ authError = _useRegistryAuth.authError,
55095
+ initiateAuth = _useRegistryAuth.initiateAuth,
55096
+ cancelAuth = _useRegistryAuth.cancelAuth;
54939
55097
  React.useEffect(function () {
54940
55098
  if (open) {
54941
55099
  setState(STATE.WELCOME);
@@ -54997,7 +55155,7 @@ var OnboardingModal = function OnboardingModal(_ref) {
54997
55155
  })), [markCompletedAndClose, onDismiss]);
54998
55156
  var handleInstall = React.useCallback(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
54999
55157
  var _window$mainApi2, _window$mainApi2$onIn;
55000
- var _window$mainApi3, _window$mainApi3$inst, result, _t2;
55158
+ var existing, _window$mainApi3, _window$mainApi3$inst, result, _t2;
55001
55159
  return _regeneratorRuntime.wrap(function (_context3) {
55002
55160
  while (1) switch (_context3.prev = _context3.next) {
55003
55161
  case 0:
@@ -55009,6 +55167,24 @@ var OnboardingModal = function OnboardingModal(_ref) {
55009
55167
  setState(STATE.ERROR);
55010
55168
  return _context3.abrupt("return");
55011
55169
  case 1:
55170
+ // Dedupe: if the user already has a Kitchen Sink workspace
55171
+ // installed from the registry, skip the install entirely and
55172
+ // route them to the existing one. Without this, the modal
55173
+ // could be re-triggered (e.g. after clearing the
55174
+ // onboarding.completed flag) and would silently create
55175
+ // "Kitchen Sink 2", "Kitchen Sink 3", etc.
55176
+ existing = findExistingKitchenSink(workspaces);
55177
+ if (!existing) {
55178
+ _context3.next = 2;
55179
+ break;
55180
+ }
55181
+ installResultRef.current = {
55182
+ success: true,
55183
+ workspace: existing
55184
+ };
55185
+ setState(STATE.DONE);
55186
+ return _context3.abrupt("return");
55187
+ case 2:
55012
55188
  setState(STATE.INSTALLING);
55013
55189
  setProgressItems([]);
55014
55190
  setInstallError(null);
@@ -55036,42 +55212,49 @@ var OnboardingModal = function OnboardingModal(_ref) {
55036
55212
  return next;
55037
55213
  });
55038
55214
  });
55039
- _context3.prev = 2;
55040
- _context3.next = 3;
55215
+ _context3.prev = 3;
55216
+ _context3.next = 4;
55041
55217
  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, {});
55042
- case 3:
55218
+ case 4:
55043
55219
  result = _context3.sent;
55044
55220
  if (cleanupProgressRef.current) {
55045
55221
  cleanupProgressRef.current();
55046
55222
  cleanupProgressRef.current = null;
55047
55223
  }
55048
55224
  if (!(!result || !result.success)) {
55049
- _context3.next = 4;
55225
+ _context3.next = 6;
55226
+ break;
55227
+ }
55228
+ if (!(result !== null && result !== void 0 && result.authRequired)) {
55229
+ _context3.next = 5;
55050
55230
  break;
55051
55231
  }
55232
+ setState(STATE.AUTH_REQUIRED);
55233
+ return _context3.abrupt("return");
55234
+ case 5:
55052
55235
  setInstallError((result === null || result === void 0 ? void 0 : result.error) || "Failed to install Kitchen Sink. Check your internet connection and try again.");
55053
55236
  setState(STATE.ERROR);
55054
55237
  return _context3.abrupt("return");
55055
- case 4:
55238
+ case 6:
55056
55239
  installResultRef.current = result;
55057
55240
  setState(STATE.DONE);
55058
- _context3.next = 6;
55241
+ _context3.next = 8;
55059
55242
  break;
55060
- case 5:
55061
- _context3.prev = 5;
55062
- _t2 = _context3["catch"](2);
55243
+ case 7:
55244
+ _context3.prev = 7;
55245
+ _t2 = _context3["catch"](3);
55063
55246
  if (cleanupProgressRef.current) {
55064
55247
  cleanupProgressRef.current();
55065
55248
  cleanupProgressRef.current = null;
55066
55249
  }
55067
55250
  setInstallError((_t2 === null || _t2 === void 0 ? void 0 : _t2.message) || "Installation failed.");
55068
55251
  setState(STATE.ERROR);
55069
- case 6:
55252
+ case 8:
55070
55253
  case "end":
55071
55254
  return _context3.stop();
55072
55255
  }
55073
- }, _callee3, null, [[2, 5]]);
55074
- })), [appId]);
55256
+ }, _callee3, null, [[3, 7]]);
55257
+ })), [appId, workspaces]);
55075
55258
  var handleOpen = React.useCallback(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee4() {
55076
55259
  var _installResultRef$cur;
55077
55260
  var workspace;
@@ -55096,6 +55279,17 @@ var OnboardingModal = function OnboardingModal(_ref) {
55096
55279
  handleInstall();
55097
55280
  }, [handleInstall]);
55098
55281
 
55282
+ // From the AUTH_REQUIRED state: kick off device-code OAuth. The
55283
+ // hook opens the browser, polls in the background, and fires our
55284
+ // onAuthorized callback on success. We auto-retry the install
55285
+ // there so the user lands on the DONE state without a second
55286
+ // click.
55287
+ var handleSignIn = React.useCallback(function () {
55288
+ initiateAuth(function () {
55289
+ handleInstall();
55290
+ });
55291
+ }, [initiateAuth, handleInstall]);
55292
+
55099
55293
  // The Modal dispatches setIsOpen(false) on Escape / backdrop click.
55100
55294
  // Route any close attempt through handleSkip so the completion flag
55101
55295
  // is always stamped (otherwise Escape would silently re-show the
@@ -55119,6 +55313,15 @@ var OnboardingModal = function OnboardingModal(_ref) {
55119
55313
  onInstall: handleInstall,
55120
55314
  onSkip: handleSkip,
55121
55315
  textMuted: textMuted
55316
+ }), state === STATE.AUTH_REQUIRED && /*#__PURE__*/jsxRuntime.jsx(AuthRequiredBody, {
55317
+ onSignIn: handleSignIn,
55318
+ onCancelAuth: cancelAuth,
55319
+ onSkip: handleSkip,
55320
+ isAuthenticating: isAuthenticating,
55321
+ authFlow: authFlow,
55322
+ authError: authError,
55323
+ textMuted: textMuted,
55324
+ borderPanel: borderPanel
55122
55325
  }), state === STATE.INSTALLING && /*#__PURE__*/jsxRuntime.jsx(InstallingBody, {
55123
55326
  items: progressItems,
55124
55327
  borderPanel: borderPanel,
@@ -55148,11 +55351,13 @@ function WelcomeBody(_ref7) {
55148
55351
  }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Heading2, {
55149
55352
  title: "Welcome to Dash"
55150
55353
  })]
55151
- }), /*#__PURE__*/jsxRuntime.jsxs(DashReact.Paragraph, {
55354
+ }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Paragraph, {
55152
55355
  className: "".concat(textMuted, " mb-6"),
55153
- children: ["Get started with the ", /*#__PURE__*/jsxRuntime.jsx("strong", {
55154
- children: "Kitchen Sink"
55155
- }), " 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."]
55356
+ children: /*#__PURE__*/jsxRuntime.jsxs("span", {
55357
+ children: ["Get started with the ", /*#__PURE__*/jsxRuntime.jsx("strong", {
55358
+ children: "Kitchen Sink"
55359
+ }), " 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."]
55360
+ })
55156
55361
  }), /*#__PURE__*/jsxRuntime.jsxs("div", {
55157
55362
  className: "flex items-center justify-end gap-3 mt-2",
55158
55363
  children: [/*#__PURE__*/jsxRuntime.jsx(DashReact.Button, {
@@ -55313,6 +55518,153 @@ function ErrorBody(_ref1) {
55313
55518
  })]
55314
55519
  });
55315
55520
  }
55521
+ function BenefitRow(_ref10) {
55522
+ var icon = _ref10.icon,
55523
+ title = _ref10.title,
55524
+ description = _ref10.description,
55525
+ textMuted = _ref10.textMuted;
55526
+ return /*#__PURE__*/jsxRuntime.jsxs("div", {
55527
+ className: "flex items-start gap-3",
55528
+ children: [/*#__PURE__*/jsxRuntime.jsx("div", {
55529
+ className: "flex-shrink-0 w-7 h-7 flex items-center justify-center mt-0.5",
55530
+ children: /*#__PURE__*/jsxRuntime.jsx(DashReact.FontAwesomeIcon, {
55531
+ icon: icon,
55532
+ className: "text-base opacity-70"
55533
+ })
55534
+ }), /*#__PURE__*/jsxRuntime.jsxs("div", {
55535
+ className: "flex flex-col",
55536
+ children: [/*#__PURE__*/jsxRuntime.jsx("span", {
55537
+ className: "text-sm font-medium",
55538
+ children: title
55539
+ }), /*#__PURE__*/jsxRuntime.jsx("span", {
55540
+ className: "text-xs ".concat(textMuted),
55541
+ children: description
55542
+ })]
55543
+ })]
55544
+ });
55545
+ }
55546
+ function AuthRequiredBody(_ref11) {
55547
+ var onSignIn = _ref11.onSignIn,
55548
+ onCancelAuth = _ref11.onCancelAuth,
55549
+ onSkip = _ref11.onSkip,
55550
+ isAuthenticating = _ref11.isAuthenticating,
55551
+ authFlow = _ref11.authFlow,
55552
+ authError = _ref11.authError,
55553
+ textMuted = _ref11.textMuted,
55554
+ borderPanel = _ref11.borderPanel;
55555
+ // While the device-code flow is polling, swap the benefits panel
55556
+ // for the user-code + verification-URL display so the user knows
55557
+ // what to enter in the browser tab we just opened.
55558
+ if (isAuthenticating && authFlow) {
55559
+ return /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
55560
+ children: [/*#__PURE__*/jsxRuntime.jsxs("div", {
55561
+ className: "flex items-center gap-3 mb-2",
55562
+ children: [/*#__PURE__*/jsxRuntime.jsx(DashReact.FontAwesomeIcon, {
55563
+ icon: "circle-notch",
55564
+ className: "text-2xl animate-spin"
55565
+ }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Heading2, {
55566
+ title: "Waiting for browser sign-in"
55567
+ })]
55568
+ }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Paragraph, {
55569
+ className: "".concat(textMuted, " mb-4"),
55570
+ children: /*#__PURE__*/jsxRuntime.jsx("span", {
55571
+ children: "We opened a browser tab where you can sign in to the Dash Registry. Enter this code if prompted:"
55572
+ })
55573
+ }), /*#__PURE__*/jsxRuntime.jsxs("div", {
55574
+ className: "border ".concat(borderPanel, " rounded-md p-4 mb-4 text-center"),
55575
+ "data-testid": "onboarding-auth-user-code",
55576
+ children: [/*#__PURE__*/jsxRuntime.jsx("div", {
55577
+ className: "text-2xl font-mono tracking-widest",
55578
+ children: authFlow.userCode || "—"
55579
+ }), /*#__PURE__*/jsxRuntime.jsx("div", {
55580
+ className: "text-xs ".concat(textMuted, " mt-2 break-all"),
55581
+ children: authFlow.verificationUrlComplete || authFlow.verificationUrl || ""
55582
+ })]
55583
+ }), /*#__PURE__*/jsxRuntime.jsx("div", {
55584
+ className: "flex items-center justify-end gap-3",
55585
+ children: /*#__PURE__*/jsxRuntime.jsx(DashReact.Button, {
55586
+ onClick: onCancelAuth,
55587
+ title: "Cancel",
55588
+ textSize: "text-sm",
55589
+ padding: "py-2 px-4",
55590
+ backgroundColor: "bg-gray-700",
55591
+ textColor: "text-gray-300",
55592
+ hoverTextColor: "hover:text-white",
55593
+ hoverBackgroundColor: "hover:bg-gray-600",
55594
+ "data-testid": "onboarding-auth-cancel-button"
55595
+ })
55596
+ })]
55597
+ });
55598
+ }
55599
+ return /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
55600
+ children: [/*#__PURE__*/jsxRuntime.jsxs("div", {
55601
+ className: "flex items-center gap-3 mb-2",
55602
+ children: [/*#__PURE__*/jsxRuntime.jsx(DashReact.FontAwesomeIcon, {
55603
+ icon: "user-plus",
55604
+ className: "text-2xl"
55605
+ }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Heading2, {
55606
+ title: "Sign in to the Dash Registry"
55607
+ })]
55608
+ }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Paragraph, {
55609
+ className: "".concat(textMuted, " mb-5"),
55610
+ children: /*#__PURE__*/jsxRuntime.jsx("span", {
55611
+ 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."
55612
+ })
55613
+ }), /*#__PURE__*/jsxRuntime.jsxs("div", {
55614
+ className: "grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6",
55615
+ children: [/*#__PURE__*/jsxRuntime.jsx(BenefitRow, {
55616
+ icon: "cube",
55617
+ title: "Install widgets",
55618
+ description: "Browse and one-click install community widgets",
55619
+ textMuted: textMuted
55620
+ }), /*#__PURE__*/jsxRuntime.jsx(BenefitRow, {
55621
+ icon: "table-cells",
55622
+ title: "Discover dashboards",
55623
+ description: "Like Kitchen Sink \u2014 curated starters built by others",
55624
+ textMuted: textMuted
55625
+ }), /*#__PURE__*/jsxRuntime.jsx(BenefitRow, {
55626
+ icon: "palette",
55627
+ title: "Apply themes",
55628
+ description: "Community-built themes for any taste",
55629
+ textMuted: textMuted
55630
+ }), /*#__PURE__*/jsxRuntime.jsx(BenefitRow, {
55631
+ icon: "upload",
55632
+ title: "Publish your own",
55633
+ description: "Share your widgets and dashboards with the Dash community",
55634
+ textMuted: textMuted
55635
+ })]
55636
+ }), authError && /*#__PURE__*/jsxRuntime.jsx(DashReact.Paragraph, {
55637
+ className: "text-red-400 mb-4",
55638
+ children: /*#__PURE__*/jsxRuntime.jsx("span", {
55639
+ children: authError
55640
+ })
55641
+ }), /*#__PURE__*/jsxRuntime.jsxs("div", {
55642
+ className: "flex items-center justify-end gap-3 mt-2",
55643
+ children: [/*#__PURE__*/jsxRuntime.jsx(DashReact.Button, {
55644
+ onClick: onSkip,
55645
+ title: "Skip for now",
55646
+ textSize: "text-sm",
55647
+ padding: "py-2 px-4",
55648
+ backgroundColor: "bg-gray-700",
55649
+ textColor: "text-gray-300",
55650
+ hoverTextColor: "hover:text-white",
55651
+ hoverBackgroundColor: "hover:bg-gray-600",
55652
+ "data-testid": "onboarding-auth-skip-button"
55653
+ }), /*#__PURE__*/jsxRuntime.jsx(DashReact.Button, {
55654
+ onClick: onSignIn,
55655
+ title: "Sign in to Registry",
55656
+ textSize: "text-sm",
55657
+ padding: "py-2 px-4",
55658
+ backgroundColor: "bg-blue-600",
55659
+ textColor: "text-white",
55660
+ hoverTextColor: "hover:text-white",
55661
+ hoverBackgroundColor: "hover:bg-blue-500",
55662
+ icon: "arrow-up-right-from-square",
55663
+ "data-testid": "onboarding-auth-signin-button"
55664
+ })]
55665
+ })]
55666
+ });
55667
+ }
55316
55668
 
55317
55669
  var DashCommandPalette = function DashCommandPalette(_ref) {
55318
55670
  var isOpen = _ref.isOpen,
@@ -61326,7 +61678,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61326
61678
  onboardingCheckedRef.current = true;
61327
61679
  var cancelled = false;
61328
61680
  _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
61329
- var _window$mainApi, _window$mainApi$getSt, status;
61681
+ var _window$mainApi, _window$mainApi$getSt, status, alreadyHasKitchenSink;
61330
61682
  return _regeneratorRuntime.wrap(function (_context) {
61331
61683
  while (1) switch (_context.prev = _context.next) {
61332
61684
  case 0:
@@ -61341,7 +61693,20 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
61341
61693
  }
61342
61694
  return _context.abrupt("return");
61343
61695
  case 2:
61344
- if (status && status.completed === false && workspaceConfig.length === 0) {
61696
+ // Defense-in-depth: even if `workspaceConfig.length === 0`
61697
+ // briefly reads true during a race between `isLoadingWorkspaces`
61698
+ // and the workspace array populating, don't show the modal
61699
+ // if the user already has a Kitchen Sink dashboard installed
61700
+ // from the registry. Same dedupe rule the modal's install
61701
+ // path applies.
61702
+ alreadyHasKitchenSink = workspaceConfig.some(function (ws) {
61703
+ var _ws$_dashboardConfig;
61704
+ var pkg = ws === null || ws === void 0 || (_ws$_dashboardConfig = ws._dashboardConfig) === null || _ws$_dashboardConfig === void 0 ? void 0 : _ws$_dashboardConfig.registryPackage;
61705
+ if (typeof pkg !== "string") return false;
61706
+ var trailing = pkg.includes("/") ? pkg.split("/").pop() : pkg;
61707
+ return trailing === "kitchen-sink";
61708
+ });
61709
+ if (status && status.completed === false && workspaceConfig.length === 0 && !alreadyHasKitchenSink) {
61345
61710
  setIsOnboardingOpen(true);
61346
61711
  } else {
61347
61712
  setIsOnboardingOpen(false);
@@ -63145,6 +63510,7 @@ var DashboardStageInner = function DashboardStageInner(_ref3) {
63145
63510
  }), /*#__PURE__*/jsxRuntime.jsx(OnboardingModal, {
63146
63511
  open: isOnboardingOpen === true,
63147
63512
  appId: credentials === null || credentials === void 0 ? void 0 : credentials.appId,
63513
+ workspaces: workspaceConfig,
63148
63514
  onDismiss: function onDismiss() {
63149
63515
  return setIsOnboardingOpen(false);
63150
63516
  },