@snowcone-app/ui 0.8.2 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2936](https://github.com/snowcone-app/snowcone-monorepo/pull/2936) [`3c37e27`](https://github.com/snowcone-app/snowcone-monorepo/commit/3c37e27da40c8865b4c4bd71ec9454ec7e64bc4c) Thanks [@kevinsproles](https://github.com/kevinsproles)! - `AddToCart` waits for your add and never reports a failure as success. An async `onClick` is awaited: the button shows
8
+ "Added!" only after it resolves, and a rejection (or throw) shows `errorText` (new prop, default
9
+ "Couldn't add — try again"), announces the error's message in a `role="alert"`, calls `onError`, and leaves the button
10
+ enabled to retry. Previously `onClick` was called without awaiting after a fixed simulated delay, so a failed add still
11
+ said "Added!". The button exposes `data-add-to-cart="idle | adding | added | error"` (type `AddToCartStatus`) for tests.
12
+ `simulateDelay` now applies only when there is no `onClick` (demos).
13
+
14
+ ## 0.8.3
15
+
16
+ ### Patch Changes
17
+
18
+ - [#3131](https://github.com/snowcone-app/snowcone-monorepo/pull/3131) [`84f6ac3`](https://github.com/snowcone-app/snowcone-monorepo/commit/84f6ac38e54c57c483ff49c18548cd9ba832d30e) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime `spreadCrops` pieces can carry `part: { index, count }`: a placement cut into strips that sit on different spread artboards is assembled server-side once every strip has arrived.
19
+
20
+ - Updated dependencies [[`84f6ac3`](https://github.com/snowcone-app/snowcone-monorepo/commit/84f6ac38e54c57c483ff49c18548cd9ba832d30e)]:
21
+ - @snowcone-app/sdk@0.24.2
22
+
3
23
  ## 0.8.2
4
24
 
5
25
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -342,6 +342,7 @@ __export(src_exports, {
342
342
  isWideMonitor: () => isWideMonitor,
343
343
  kbdVariants: () => kbdVariants,
344
344
  linkVariants: () => linkVariants,
345
+ linkedPlacementsLabel: () => linkedPlacementsLabel,
345
346
  listProducts: () => import_sdk11.listProducts,
346
347
  normalizeHex: () => normalizeHex,
347
348
  observe: () => observe,
@@ -4920,6 +4921,7 @@ function AddToCart({
4920
4921
  text = "Add to Bag",
4921
4922
  loadingText = "Adding...",
4922
4923
  successText = "Added!",
4924
+ errorText = "Couldn't add \u2014 try again",
4923
4925
  disabled = false,
4924
4926
  className = "",
4925
4927
  quantity = 1,
@@ -4929,8 +4931,10 @@ function AddToCart({
4929
4931
  }) {
4930
4932
  const context = useProduct();
4931
4933
  const shopContext = useShopOptional();
4932
- const [loading, setLoading] = (0, import_react15.useState)(false);
4933
- const [success, setSuccess] = (0, import_react15.useState)(false);
4934
+ const [status, setStatus] = (0, import_react15.useState)("idle");
4935
+ const [errorMessage, setErrorMessage] = (0, import_react15.useState)(null);
4936
+ const loading = status === "adding";
4937
+ const success = status === "added";
4934
4938
  (0, import_react15.useEffect)(() => {
4935
4939
  checkProviders(
4936
4940
  "AddToCart",
@@ -4953,7 +4957,8 @@ function AddToCart({
4953
4957
  const handleClick = async () => {
4954
4958
  if (isDisabled || !product) return;
4955
4959
  try {
4956
- setLoading(true);
4960
+ setStatus("adding");
4961
+ setErrorMessage(null);
4957
4962
  const cartDetail = (0, import_sdk7.createCartDetail)({
4958
4963
  productId: product.id,
4959
4964
  quantity,
@@ -4964,20 +4969,23 @@ function AddToCart({
4964
4969
  if (!cartDetail) {
4965
4970
  throw new Error("Invalid selection - unable to create cart detail");
4966
4971
  }
4967
- await (0, import_sdk7.simulateCartOperation)(simulateDelay);
4968
- onClick?.(cartDetail);
4969
- setSuccess(true);
4972
+ if (onClick) {
4973
+ await onClick(cartDetail);
4974
+ } else {
4975
+ await (0, import_sdk7.simulateCartOperation)(simulateDelay);
4976
+ }
4977
+ setStatus("added");
4970
4978
  setTimeout(() => {
4971
- setSuccess(false);
4979
+ setStatus((s) => s === "added" ? "idle" : s);
4972
4980
  }, 2e3);
4973
4981
  } catch (error) {
4974
- onError?.(error);
4975
- console.error("Add to cart failed:", error);
4976
- } finally {
4977
- setLoading(false);
4982
+ const err = error instanceof Error ? error : new Error(String(error));
4983
+ setStatus("error");
4984
+ setErrorMessage(err.message);
4985
+ onError?.(err);
4978
4986
  }
4979
4987
  };
4980
- const buttonText = loading ? loadingText : success ? successText : text;
4988
+ const buttonText = loading ? loadingText : success ? successText : status === "error" ? errorText : text;
4981
4989
  const defaultStyles = "w-full bg-primary text-primary-foreground hover:bg-primary/90 px-6 py-3 rounded-button font-medium transition-colors";
4982
4990
  const buttonClasses = `
4983
4991
  ${defaultStyles}
@@ -4988,6 +4996,7 @@ function AddToCart({
4988
4996
  const getAriaLabel = () => {
4989
4997
  if (loading) return loadingText;
4990
4998
  if (success) return successText;
4999
+ if (status === "error") return `${errorText} - ${product?.name || "product"}`;
4991
5000
  if (!product) return text;
4992
5001
  return `${text} - ${product.name || "product"}`;
4993
5002
  };
@@ -5001,9 +5010,11 @@ function AddToCart({
5001
5010
  onClick: handleClick,
5002
5011
  "aria-label": getAriaLabel(),
5003
5012
  "aria-busy": loading,
5013
+ "data-add-to-cart": status,
5004
5014
  children: buttonText
5005
5015
  }
5006
- )
5016
+ ),
5017
+ status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("p", { role: "alert", className: "mt-2 text-sm text-destructive", children: errorMessage })
5007
5018
  ] });
5008
5019
  }
5009
5020
 
@@ -5750,6 +5761,11 @@ function PlacementClipShapeSelector() {
5750
5761
  // src/composed/PlacementTabs.tsx
5751
5762
  var import_react21 = require("react");
5752
5763
  var import_jsx_runtime50 = require("react/jsx-runtime");
5764
+ function linkedPlacementsLabel(count) {
5765
+ return count === 2 ? "Both sides" : `All ${count} areas`;
5766
+ }
5767
+ var ACTION_CLASS = "transition-colors duration-200 cursor-pointer text-muted-foreground hover:text-foreground underline underline-offset-4 decoration-muted-foreground/40 hover:decoration-foreground";
5768
+ var PILL_CLASS = "inline-flex items-center rounded-full bg-foreground/[0.08] px-3 py-1 font-medium text-foreground";
5753
5769
  function PlacementTabs({
5754
5770
  placements,
5755
5771
  selectedPlacement,
@@ -5757,7 +5773,13 @@ function PlacementTabs({
5757
5773
  isDesktop = false,
5758
5774
  toolbarPlaceholderId = "toolbar-placeholder",
5759
5775
  toolbarSize = 120,
5760
- className = ""
5776
+ className = "",
5777
+ linked,
5778
+ onSplitAll,
5779
+ onLinkAll,
5780
+ linkedLabel,
5781
+ splitAllLabel = "Customize separately",
5782
+ linkAllLabel = "Use one design for all"
5761
5783
  }) {
5762
5784
  const containerRef = (0, import_react21.useRef)(null);
5763
5785
  const [lineHeight, setLineHeight] = (0, import_react21.useState)(24);
@@ -5774,7 +5796,7 @@ function PlacementTabs({
5774
5796
  if (isDesktop) {
5775
5797
  return null;
5776
5798
  }
5777
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: `flex justify-end w-full ${className}`, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5799
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: `flex justify-end w-full ${className}`, "data-placement-tabs": true, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5778
5800
  "span",
5779
5801
  {
5780
5802
  id: toolbarPlaceholderId,
@@ -5783,16 +5805,68 @@ function PlacementTabs({
5783
5805
  }
5784
5806
  ) });
5785
5807
  }
5808
+ const dataLinked = linked === void 0 ? void 0 : linked ? "true" : "false";
5809
+ const labels = placements.map((p) => p.label);
5810
+ const renderItems = (tabClass) => {
5811
+ if (linked === true) {
5812
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
5813
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5814
+ "span",
5815
+ {
5816
+ className: `${tabClass} ${PILL_CLASS}`,
5817
+ "data-placement-pill": true,
5818
+ title: labels.join(" \xB7 "),
5819
+ "aria-label": `One design on ${labels.join(", ")}`,
5820
+ children: linkedLabel ?? linkedPlacementsLabel(placements.length)
5821
+ }
5822
+ ),
5823
+ onSplitAll && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5824
+ "button",
5825
+ {
5826
+ type: "button",
5827
+ onClick: onSplitAll,
5828
+ className: `${tabClass} ${ACTION_CLASS}`,
5829
+ "data-action": "split-all",
5830
+ children: splitAllLabel
5831
+ }
5832
+ )
5833
+ ] });
5834
+ }
5835
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
5836
+ placements.map((p) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5837
+ "button",
5838
+ {
5839
+ type: "button",
5840
+ onClick: () => onPlacementChange(p.label),
5841
+ "data-placement-tab": p.label,
5842
+ "aria-pressed": selectedPlacement === p.label,
5843
+ className: `${tabClass} transition-all duration-200 cursor-pointer border-b-2 ${selectedPlacement === p.label ? "text-foreground border-primary" : "text-foreground/70 hover:text-foreground border-muted-foreground/20 hover:border-muted-foreground/40"}`,
5844
+ children: p.label
5845
+ },
5846
+ p.label
5847
+ )),
5848
+ linked === false && onLinkAll && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5849
+ "button",
5850
+ {
5851
+ type: "button",
5852
+ onClick: onLinkAll,
5853
+ className: `${tabClass} ${ACTION_CLASS}`,
5854
+ "data-action": "link-all",
5855
+ children: linkAllLabel
5856
+ }
5857
+ )
5858
+ ] });
5859
+ };
5786
5860
  if (isDesktop) {
5787
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: `flex flex-wrap gap-x-3 text-sm leading-relaxed w-full ${className}`, children: placements.map((p) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5788
- "button",
5861
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5862
+ "div",
5789
5863
  {
5790
- onClick: () => onPlacementChange(p.label),
5791
- className: `transition-all duration-200 cursor-pointer border-b-2 ${selectedPlacement === p.label ? "text-foreground border-primary" : "text-foreground/70 hover:text-foreground border-muted-foreground/20 hover:border-muted-foreground/40"}`,
5792
- children: p.label
5793
- },
5794
- p.label
5795
- )) });
5864
+ className: `flex flex-wrap items-center gap-x-3 gap-y-1 text-sm leading-relaxed w-full ${className}`,
5865
+ "data-placement-tabs": true,
5866
+ "data-linked": dataLinked,
5867
+ children: renderItems("")
5868
+ }
5869
+ );
5796
5870
  }
5797
5871
  const linesForToolbar = Math.ceil(toolbarSize / lineHeight);
5798
5872
  const shapeInsetPx = linesForToolbar * lineHeight;
@@ -5801,6 +5875,8 @@ function PlacementTabs({
5801
5875
  {
5802
5876
  ref: containerRef,
5803
5877
  className: `flex overflow-hidden text-sm leading-relaxed w-full ${className}`,
5878
+ "data-placement-tabs": true,
5879
+ "data-linked": dataLinked,
5804
5880
  children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("p", { className: "m-0 w-full", children: [
5805
5881
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5806
5882
  "span",
@@ -5819,15 +5895,7 @@ function PlacementTabs({
5819
5895
  )
5820
5896
  }
5821
5897
  ),
5822
- placements.map((p) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
5823
- "button",
5824
- {
5825
- onClick: () => onPlacementChange(p.label),
5826
- className: `inline transition-all duration-200 cursor-pointer mr-3 border-b-2 ${selectedPlacement === p.label ? "text-foreground border-primary" : "text-foreground/70 hover:text-foreground border-muted-foreground/20 hover:border-muted-foreground/40"}`,
5827
- children: p.label
5828
- },
5829
- p.label
5830
- ))
5898
+ renderItems("inline mr-3")
5831
5899
  ] })
5832
5900
  }
5833
5901
  );
@@ -16554,6 +16622,7 @@ function usePersonalizationShimmer(containerRef, safetyTimeoutMs = 1e4) {
16554
16622
  isWideMonitor,
16555
16623
  kbdVariants,
16556
16624
  linkVariants,
16625
+ linkedPlacementsLabel,
16557
16626
  listProducts,
16558
16627
  normalizeHex,
16559
16628
  observe,