@sableui/react 0.4.0 → 0.6.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.
Files changed (90) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +189 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/THIRD_PARTY_NOTICES.md +11 -0
  5. package/bin/sable-lint.mjs +2 -1
  6. package/bin/sable.mjs +3 -2
  7. package/dist/{_contract-Cn3lUpih.d.ts → _contract-C2M1SjBf.d.ts} +20 -2
  8. package/dist/blocks/index.js +21 -53
  9. package/dist/{chunk-CCUDFLU3.js → chunk-7QSDYBXO.js} +652 -38
  10. package/dist/{chunk-FBSL2LZQ.js → chunk-EDXLM7YL.js} +11 -30
  11. package/dist/chunk-FKZXAFIY.js +12 -0
  12. package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
  13. package/dist/{chunk-NGBDP3BM.js → chunk-QJ6FWTUG.js} +1 -1
  14. package/dist/{chunk-D2ETQBHV.js → chunk-VVW4UCDI.js} +5 -5
  15. package/dist/{chunk-TH563WLW.js → chunk-XAZGMY73.js} +10 -3
  16. package/dist/foundations/index.js +6 -0
  17. package/dist/icon/index.js +2 -2
  18. package/dist/icon-sets/brands.d.ts +0 -27
  19. package/dist/icon-sets/brands.js +7 -1
  20. package/dist/icon-sets/reicon.js +1 -1
  21. package/dist/index.d.ts +164 -23
  22. package/dist/index.js +116 -29
  23. package/dist/ios/index.d.ts +6 -1
  24. package/dist/ios/index.js +4 -3
  25. package/dist/ios/styles.css +9 -0
  26. package/dist/patterns/index.js +24 -30
  27. package/dist/presets/index.d.ts +108 -5
  28. package/dist/presets/index.js +239 -6
  29. package/dist/styles.css +57 -0
  30. package/dist/{theme-provider-CGeiusgu.d.ts → theme-provider-kQszHYji.d.ts} +1 -1
  31. package/dist/themes/atelier/atelier.css +31 -3
  32. package/dist/themes/atelier/index.d.ts +1 -1
  33. package/dist/themes/ember/ember.css +38 -7
  34. package/dist/themes/ember/index.d.ts +1 -1
  35. package/dist/themes/index.d.ts +2 -2
  36. package/dist/themes/meridian/index.d.ts +1 -1
  37. package/dist/themes/meridian/meridian.css +33 -4
  38. package/dist/themes/poppy/index.d.ts +1 -1
  39. package/dist/themes/poppy/poppy.css +28 -0
  40. package/dist/themes/prism/index.d.ts +1 -1
  41. package/dist/themes/prism/prism.css +26 -0
  42. package/dist/themes/sable/index.d.ts +1 -1
  43. package/dist/themes/sable/sable.css +24 -11
  44. package/dist/themes/verdant/index.d.ts +1 -1
  45. package/dist/themes/verdant/verdant.css +34 -4
  46. package/llms/blocks/danger-zone-block.md +1 -1
  47. package/llms/blocks/event-card-block.md +1 -1
  48. package/llms/blocks/feedback-form-block.md +1 -1
  49. package/llms/blocks/invite-team-block.md +1 -1
  50. package/llms/blocks/media-player-block.md +3 -16
  51. package/llms/blocks/notification-list-block.md +3 -3
  52. package/llms/blocks/product-card-block.md +3 -17
  53. package/llms/blocks/profile-card-block.md +4 -17
  54. package/llms/blocks/profile-form-block.md +2 -2
  55. package/llms/blocks/team-members-block.md +1 -1
  56. package/llms/blocks/testimonial-block.md +1 -1
  57. package/llms/components/avatar.md +13 -1
  58. package/llms/components/card.md +1 -1
  59. package/llms/components/gradient-art.md +80 -0
  60. package/llms/components/sheet.md +1 -1
  61. package/llms/components/tabs.md +1 -1
  62. package/llms/components/toast.md +12 -1
  63. package/llms/foundations/accessibility.md +1 -0
  64. package/llms/patterns/auth-page.md +5 -19
  65. package/llms/patterns/brand-landing-page.md +1 -1
  66. package/llms/patterns/chat-panel.md +1 -1
  67. package/llms/patterns/dashboard-page.md +4 -4
  68. package/llms/patterns/feed-page.md +4 -4
  69. package/llms/patterns/landing-page.md +1 -1
  70. package/llms/patterns/list-detail-page.md +3 -2
  71. package/llms/patterns/settings-page.md +3 -3
  72. package/llms/reference/token-intent.md +9 -1
  73. package/llms/tasks/auditing-an-app.md +6 -0
  74. package/llms/tasks/building-an-ios-prototype.md +3 -1
  75. package/llms/tasks/converting-an-app.md +1 -1
  76. package/llms/tasks/creating-a-preset.md +24 -0
  77. package/llms/tasks/reviewing-a-design.md +9 -0
  78. package/llms/tasks/using-presets.md +6 -1
  79. package/llms-full.txt +214 -106
  80. package/llms.txt +4 -3
  81. package/package.json +9 -7
  82. package/registry.json +310 -32
  83. package/scripts/design-presets.mjs +27 -4
  84. package/scripts/grade.mjs +2 -1
  85. package/scripts/label-contrast.mjs +138 -0
  86. package/scripts/lint-rules.mjs +23 -2
  87. package/skills/sable/SKILL.md +3 -1
  88. package/targets/ios.json +18 -2
  89. package/third-party/paper-shaders-LICENSE.txt +201 -0
  90. package/third-party/paper-shaders-NOTICE.txt +5 -0
@@ -1,5 +1,6 @@
1
+ import { StackedActionsContext, CardDepthContext, usePresentation, PresentationProvider } from './chunk-FKZXAFIY.js';
1
2
  import { cn } from './chunk-SRQWQUTW.js';
2
- import { Icon } from './chunk-NGBDP3BM.js';
3
+ import { Icon } from './chunk-QJ6FWTUG.js';
3
4
  import { createContext, useMemo, useState, useRef, useLayoutEffect, useEffect, useSyncExternalStore, useCallback, useContext, isValidElement, Children, useId, cloneElement } from 'react';
4
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
6
  import { cva } from 'class-variance-authority';
@@ -11,6 +12,7 @@ import { Input as Input$1 } from '@base-ui/react/input';
11
12
  import { Field as Field$1 } from '@base-ui/react/field';
12
13
  import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
13
14
  import { Switch as Switch$1 } from '@base-ui/react/switch';
15
+ import { grainGradientMeta, staticMeshGradientMeta, getShaderNoiseTexture, grainGradientFragmentShader, staticMeshGradientFragmentShader } from '@paper-design/shaders';
14
16
  import { Avatar as Avatar$1 } from '@base-ui/react/avatar';
15
17
  import { Progress as Progress$1 } from '@base-ui/react/progress';
16
18
  import { Select as Select$1 } from '@base-ui/react/select';
@@ -462,7 +464,8 @@ function Button({
462
464
  }) {
463
465
  const tier = useTypeTier();
464
466
  const isNativeButton = render === void 0;
465
- const resolvedSize = size ?? "md";
467
+ const stacked = useContext(StackedActionsContext) && !String(size ?? "md").startsWith("icon");
468
+ const resolvedSize = stacked ? "cta" : size ?? "md";
466
469
  const hasLabel = children != null && children !== false;
467
470
  const spinner = /* @__PURE__ */ jsx(Spinner, { className: "size-(--button-icon)", thickness: 2 });
468
471
  const start = loading ? spinner : iconStart;
@@ -486,7 +489,8 @@ function Button({
486
489
  props: {
487
490
  ...props,
488
491
  className: cn(
489
- buttonVariants({ variant, size }),
492
+ buttonVariants({ variant, size: resolvedSize }),
493
+ stacked ? "w-full" : null,
490
494
  tier.action,
491
495
  optical,
492
496
  // Busy is not disabled. Loading sets `disabled` so the action cannot
@@ -793,7 +797,9 @@ function Card({
793
797
  ref,
794
798
  ...props
795
799
  }) {
796
- return useRender({
800
+ const depth = useContext(CardDepthContext);
801
+ const bleed = usePresentation() === "sheet" && variant === "flat" && depth === 0;
802
+ const element = useRender({
797
803
  render: render ?? /* @__PURE__ */ jsx("div", {}),
798
804
  ref,
799
805
  props: {
@@ -818,10 +824,15 @@ function Card({
818
824
  INTERACTIVE_HOVER[variant],
819
825
  className
820
826
  ),
821
- style: { "--card-p": DENSITY_PAD[density], ...style }
827
+ style: {
828
+ "--card-p": DENSITY_PAD[density],
829
+ ...bleed ? { margin: "calc(var(--card-p) * -1)", width: "calc(100% + var(--card-p) * 2)", maxWidth: "none" } : null,
830
+ ...style
831
+ }
822
832
  },
823
833
  defaultTagName: "div"
824
834
  });
835
+ return /* @__PURE__ */ jsx(CardDepthContext.Provider, { value: depth + 1, children: element });
825
836
  }
826
837
  Card.displayName = "Card";
827
838
  function CardHeader({
@@ -837,8 +848,11 @@ function CardHeader({
837
848
  // one it collapses to a single stacked column (same as before).
838
849
  // Zeroes its own bottom padding so it owns the CardHeader/CardContent
839
850
  // seam — CardContent always has full padding on its own (see below),
840
- // so a standalone CardContent never loses its top padding.
841
- "grid auto-rows-min grid-cols-1 items-start gap-1.5 p-[var(--card-p,--spacing(6))] pb-0 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
851
+ // so a standalone CardContent never loses its top padding. As the
852
+ // card's last child (a stat card: label, number, sparkline) nothing
853
+ // follows to pad the bottom, so it keeps its own; zeroed, the number
854
+ // sat on the card's bottom edge.
855
+ "grid auto-rows-min grid-cols-1 items-start gap-1.5 p-[var(--card-p,--spacing(6))] pb-0 last:pb-[var(--card-p,--spacing(6))] has-data-[slot=card-action]:grid-cols-[1fr_auto]",
842
856
  className
843
857
  ),
844
858
  ...props
@@ -921,20 +935,27 @@ function CardList({
921
935
  );
922
936
  }
923
937
  function CardFooter({
938
+ actions = false,
924
939
  className,
940
+ children,
925
941
  ...props
926
942
  }) {
943
+ const depth = useContext(CardDepthContext);
944
+ const stacked = usePresentation() === "sheet" && actions && depth <= 1;
927
945
  return /* @__PURE__ */ jsx(
928
946
  "div",
929
947
  {
930
948
  "data-slot": "card-footer",
949
+ "data-actions": actions ? stacked ? "stack" : "row" : void 0,
931
950
  className: cn(
932
951
  "flex items-center gap-2 p-[var(--card-p,--spacing(6))] pt-0",
933
952
  "[[data-slot=card-header]+&]:pt-[var(--card-p,--spacing(6))]",
934
953
  "[:is([data-slot=card-list],[data-slot=card-content][data-flush])+&]:border-t [:is([data-slot=card-list],[data-slot=card-content][data-flush])+&]:border-border [:is([data-slot=card-list],[data-slot=card-content][data-flush])+&]:pt-[var(--card-p,--spacing(6))]",
935
- className
954
+ className,
955
+ stacked && "mt-auto flex-col-reverse items-stretch justify-start"
936
956
  ),
937
- ...props
957
+ ...props,
958
+ children: stacked ? /* @__PURE__ */ jsx(StackedActionsContext.Provider, { value: true, children }) : children
938
959
  }
939
960
  );
940
961
  }
@@ -1069,8 +1090,8 @@ var cardMeta = {
1069
1090
  },
1070
1091
  {
1071
1092
  name: "CardFooter",
1072
- type: 'ComponentPropsWithRef<"div">',
1073
- description: "Bottom row for actions/meta. Padding follows the density knob."
1093
+ type: 'ComponentPropsWithRef<"div"> & { actions?: boolean }',
1094
+ description: "Bottom row for actions/meta. Padding follows the density knob. `actions` marks it as the card's action row (buttons only): in a sheet (IosSheet, a bottom SheetContent) it becomes a full-width stack pinned to the bottom, primary on top; on a page nothing changes."
1074
1095
  }
1075
1096
  ],
1076
1097
  variants: {
@@ -1322,16 +1343,18 @@ var MATRIX = {
1322
1343
  // raw tone clears 4.5:1 on the page, but on its own 15% wash it did not
1323
1344
  // (light info 4.2:1, dark edition 3.6:1, most of Verdant's tones ~4.1:1).
1324
1345
  // Mixing toward --foreground moves it AWAY from the wash in both modes and
1325
- // every theme, and keeps the hue.
1346
+ // every theme, and keeps the hue. Status tones mix from their -text token,
1347
+ // not the fill: dark fills now sit low enough for a white label, and ink
1348
+ // mixed from them fell under 4.5:1 (Meridian dark destructive, 3.8:1).
1326
1349
  soft: {
1327
1350
  neutral: "bg-secondary text-secondary-foreground",
1328
1351
  standard: "bg-tone-standard/15 text-[color-mix(in_oklch,var(--tone-standard)_75%,var(--foreground))]",
1329
1352
  collectible: "bg-tone-collectible/15 text-[color-mix(in_oklch,var(--tone-collectible)_75%,var(--foreground))]",
1330
1353
  edition: "bg-tone-edition/15 text-[color-mix(in_oklch,var(--tone-edition)_75%,var(--foreground))]",
1331
- info: "bg-tone-info/15 text-[color-mix(in_oklch,var(--tone-info)_75%,var(--foreground))]",
1332
- success: "bg-success/15 text-[color-mix(in_oklch,var(--success)_75%,var(--foreground))]",
1333
- warning: "bg-tone-warning/15 text-[color-mix(in_oklch,var(--tone-warning)_75%,var(--foreground))]",
1334
- destructive: "bg-destructive/15 text-[color-mix(in_oklch,var(--destructive)_75%,var(--foreground))]"
1354
+ info: "bg-tone-info/15 text-[color-mix(in_oklch,var(--tone-info-text)_75%,var(--foreground))]",
1355
+ success: "bg-success/15 text-[color-mix(in_oklch,var(--success-text)_75%,var(--foreground))]",
1356
+ warning: "bg-tone-warning/15 text-[color-mix(in_oklch,var(--tone-warning-text)_75%,var(--foreground))]",
1357
+ destructive: "bg-destructive/15 text-[color-mix(in_oklch,var(--destructive-text)_75%,var(--foreground))]"
1335
1358
  },
1336
1359
  outline: {
1337
1360
  neutral: "border border-border text-foreground",
@@ -1636,16 +1659,16 @@ function usePortalThemeScope() {
1636
1659
  const [portal, setPortal] = useState(null);
1637
1660
  const [probed, setProbed] = useState(null);
1638
1661
  const probeRef = useRef(null);
1639
- const pending = portal !== null && probed !== portal;
1662
+ const pending2 = portal !== null && probed !== portal;
1640
1663
  useIsomorphicLayoutEffect(() => {
1641
- if (!pending || !portal) return;
1664
+ if (!pending2 || !portal) return;
1642
1665
  const probe = probeRef.current;
1643
1666
  if (probe) applyPortalThemeScope(portal, resolvePortalThemeScope(probe));
1644
1667
  setProbed(portal);
1645
- }, [pending, portal]);
1668
+ }, [pending2, portal]);
1646
1669
  return {
1647
1670
  portalRef: setPortal,
1648
- probe: pending ? /* @__PURE__ */ jsx("span", { ref: probeRef, hidden: true, "data-sable-portal-probe": "" }) : null
1671
+ probe: pending2 ? /* @__PURE__ */ jsx("span", { ref: probeRef, hidden: true, "data-sable-portal-probe": "" }) : null
1649
1672
  };
1650
1673
  }
1651
1674
  function withPortalThemeScope(Portal) {
@@ -1717,7 +1740,7 @@ function FullscreenContent({
1717
1740
  ),
1718
1741
  ...props,
1719
1742
  children: [
1720
- children,
1743
+ /* @__PURE__ */ jsx(PresentationProvider, { value: "page", children }),
1721
1744
  showCloseButton ? (
1722
1745
  // The floating Button: Sable's control over content. Its own
1723
1746
  // translucent popover chip and border keep it legible over bright
@@ -1775,7 +1798,7 @@ function CardContent2({
1775
1798
  ),
1776
1799
  ...props,
1777
1800
  children: [
1778
- scroll ? /* @__PURE__ */ jsx("div", { className: "-m-2 min-h-0 overflow-y-auto overscroll-contain p-2 scrollbar-quiet", children }) : children,
1801
+ /* @__PURE__ */ jsx(PresentationProvider, { value: "page", children: scroll ? /* @__PURE__ */ jsx("div", { className: "-m-2 min-h-0 overflow-y-auto overscroll-contain p-2 scrollbar-quiet", children }) : children }),
1779
1802
  showCloseButton ? /* @__PURE__ */ jsx(
1780
1803
  Dialog$1.Close,
1781
1804
  {
@@ -2888,7 +2911,7 @@ function Switch({ className, ...props }) {
2888
2911
  className
2889
2912
  ),
2890
2913
  ...props,
2891
- children: /* @__PURE__ */ jsx(Switch$1.Thumb, { className: "block h-5 w-5 rounded-full bg-(--switch-thumb) shadow-sm motion-interactive ease-spring data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4 data-[checked]:bg-primary-foreground" })
2914
+ children: /* @__PURE__ */ jsx(Switch$1.Thumb, { className: "block h-5 w-5 rounded-full bg-(--switch-thumb) shadow-(--knob-shadow) motion-interactive ease-spring data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4 data-[checked]:bg-primary-foreground" })
2892
2915
  }
2893
2916
  );
2894
2917
  }
@@ -3024,6 +3047,539 @@ var switchMeta = {
3024
3047
  }
3025
3048
  ]
3026
3049
  };
3050
+ var VERTEX = `#version 300 es
3051
+ precision mediump float;
3052
+
3053
+ layout(location = 0) in vec4 a_position;
3054
+
3055
+ uniform vec2 u_resolution;
3056
+ uniform float u_pixelRatio;
3057
+ uniform float u_originX;
3058
+ uniform float u_originY;
3059
+ uniform float u_worldWidth;
3060
+ uniform float u_worldHeight;
3061
+ uniform float u_fit;
3062
+ uniform float u_scale;
3063
+ uniform float u_rotation;
3064
+ uniform float u_offsetX;
3065
+ uniform float u_offsetY;
3066
+
3067
+ out vec2 v_objectUV;
3068
+ out vec2 v_objectBoxSize;
3069
+ out vec2 v_patternUV;
3070
+ out vec2 v_patternBoxSize;
3071
+
3072
+ vec3 getBoxSize(float boxRatio, vec2 givenBoxSize) {
3073
+ vec2 box = vec2(0.);
3074
+ box.x = boxRatio * min(givenBoxSize.x / boxRatio, givenBoxSize.y);
3075
+ float noFitBoxWidth = box.x;
3076
+ if (u_fit == 1.) {
3077
+ box.x = boxRatio * min(u_resolution.x / boxRatio, u_resolution.y);
3078
+ } else if (u_fit == 2.) {
3079
+ box.x = boxRatio * max(u_resolution.x / boxRatio, u_resolution.y);
3080
+ }
3081
+ box.y = box.x / boxRatio;
3082
+ return vec3(box, noFitBoxWidth);
3083
+ }
3084
+
3085
+ void main() {
3086
+ gl_Position = a_position;
3087
+
3088
+ vec2 uv = gl_Position.xy * .5;
3089
+ vec2 boxOrigin = vec2(.5 - u_originX, u_originY - .5);
3090
+ vec2 givenBoxSize = vec2(u_worldWidth, u_worldHeight);
3091
+ givenBoxSize = max(givenBoxSize, vec2(1.)) * u_pixelRatio;
3092
+ float r = u_rotation * 3.14159265358979323846 / 180.;
3093
+ mat2 graphicRotation = mat2(cos(r), sin(r), -sin(r), cos(r));
3094
+ vec2 graphicOffset = vec2(-u_offsetX, u_offsetY);
3095
+
3096
+ vec2 fixedRatioBoxGivenSize = vec2(
3097
+ (u_worldWidth == 0.) ? u_resolution.x : givenBoxSize.x,
3098
+ (u_worldHeight == 0.) ? u_resolution.y : givenBoxSize.y
3099
+ );
3100
+ v_objectBoxSize = getBoxSize(1., fixedRatioBoxGivenSize).xy;
3101
+ vec2 objectWorldScale = u_resolution.xy / v_objectBoxSize;
3102
+ v_objectUV = uv;
3103
+ v_objectUV *= objectWorldScale;
3104
+ v_objectUV += boxOrigin * (objectWorldScale - 1.);
3105
+ v_objectUV += graphicOffset;
3106
+ v_objectUV /= u_scale;
3107
+ v_objectUV = graphicRotation * v_objectUV;
3108
+
3109
+ vec2 patternBoxGivenSize = vec2(
3110
+ (u_worldWidth == 0.) ? u_resolution.x : givenBoxSize.x,
3111
+ (u_worldHeight == 0.) ? u_resolution.y : givenBoxSize.y
3112
+ );
3113
+ float patternBoxRatio = patternBoxGivenSize.x / patternBoxGivenSize.y;
3114
+ vec3 boxSizeData = getBoxSize(patternBoxRatio, patternBoxGivenSize);
3115
+ v_patternBoxSize = boxSizeData.xy;
3116
+ float patternBoxNoFitBoxWidth = boxSizeData.z;
3117
+ vec2 patternBoxScale = u_resolution.xy / v_patternBoxSize;
3118
+ v_patternUV = uv;
3119
+ v_patternUV += graphicOffset / patternBoxScale;
3120
+ v_patternUV += boxOrigin;
3121
+ v_patternUV -= boxOrigin / patternBoxScale;
3122
+ v_patternUV *= u_resolution.xy;
3123
+ v_patternUV /= u_pixelRatio;
3124
+ if (u_fit > 0.) {
3125
+ v_patternUV *= (patternBoxNoFitBoxWidth / v_patternBoxSize.x);
3126
+ }
3127
+ v_patternUV /= u_scale;
3128
+ v_patternUV = graphicRotation * v_patternUV;
3129
+ v_patternUV += boxOrigin / patternBoxScale;
3130
+ v_patternUV -= boxOrigin;
3131
+ v_patternUV *= .01;
3132
+ }
3133
+ `;
3134
+ var FRAGMENTS = { mesh: staticMeshGradientFragmentShader, grain: grainGradientFragmentShader };
3135
+ var MAX_COLORS = { mesh: staticMeshGradientMeta.maxColorCount, grain: grainGradientMeta.maxColorCount };
3136
+ var GRAIN_CORNERS = 4;
3137
+ var state;
3138
+ var noiseImage = null;
3139
+ var listeners = /* @__PURE__ */ new Set();
3140
+ function onArtInvalidate(listener) {
3141
+ listeners.add(listener);
3142
+ return () => listeners.delete(listener);
3143
+ }
3144
+ var invalidate = () => {
3145
+ for (const listener of listeners) listener();
3146
+ };
3147
+ function compile(gl, type, source) {
3148
+ const shader = gl.createShader(type);
3149
+ if (!shader) return null;
3150
+ gl.shaderSource(shader, source);
3151
+ gl.compileShader(shader);
3152
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
3153
+ console.error("GradientArt: shader failed to compile", gl.getShaderInfoLog(shader));
3154
+ gl.deleteShader(shader);
3155
+ return null;
3156
+ }
3157
+ return shader;
3158
+ }
3159
+ function programFor(s, variant) {
3160
+ const cached = s.programs[variant];
3161
+ if (cached !== void 0) return cached;
3162
+ s.programs[variant] = null;
3163
+ const { gl } = s;
3164
+ const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);
3165
+ const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENTS[variant]);
3166
+ if (!vertex || !fragment) return null;
3167
+ const program = gl.createProgram();
3168
+ if (!program) return null;
3169
+ gl.attachShader(program, vertex);
3170
+ gl.attachShader(program, fragment);
3171
+ gl.linkProgram(program);
3172
+ gl.deleteShader(vertex);
3173
+ gl.deleteShader(fragment);
3174
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
3175
+ console.error("GradientArt: shader failed to link", gl.getProgramInfoLog(program));
3176
+ return null;
3177
+ }
3178
+ const entry = { program, uniforms: /* @__PURE__ */ new Map() };
3179
+ s.programs[variant] = entry;
3180
+ return entry;
3181
+ }
3182
+ function setup(s) {
3183
+ const { gl } = s;
3184
+ const buffer = gl.createBuffer();
3185
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
3186
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
3187
+ gl.enableVertexAttribArray(0);
3188
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
3189
+ s.programs = {};
3190
+ s.noise = null;
3191
+ if (noiseImage?.complete && noiseImage.naturalWidth) uploadNoise(s, noiseImage);
3192
+ }
3193
+ function uploadNoise(s, image) {
3194
+ const { gl } = s;
3195
+ const texture = gl.createTexture();
3196
+ gl.activeTexture(gl.TEXTURE0);
3197
+ gl.bindTexture(gl.TEXTURE_2D, texture);
3198
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3199
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3200
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3201
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3202
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
3203
+ s.noise = texture;
3204
+ }
3205
+ function getState() {
3206
+ if (state !== void 0) return state;
3207
+ if (typeof document === "undefined") return null;
3208
+ const canvas = document.createElement("canvas");
3209
+ const gl = canvas.getContext("webgl2", { alpha: true, premultipliedAlpha: true, antialias: false, preserveDrawingBuffer: true });
3210
+ if (!gl) {
3211
+ state = null;
3212
+ return state;
3213
+ }
3214
+ const s = { canvas, gl, programs: {}, noise: null, lost: false };
3215
+ setup(s);
3216
+ canvas.addEventListener("webglcontextlost", (event) => {
3217
+ event.preventDefault();
3218
+ s.lost = true;
3219
+ });
3220
+ canvas.addEventListener("webglcontextrestored", () => {
3221
+ s.lost = false;
3222
+ setup(s);
3223
+ invalidate();
3224
+ });
3225
+ state = s;
3226
+ return state;
3227
+ }
3228
+ function noiseReady(s) {
3229
+ if (s.noise) return true;
3230
+ if (!noiseImage) {
3231
+ noiseImage = getShaderNoiseTexture() ?? null;
3232
+ noiseImage?.decode().then(
3233
+ () => {
3234
+ const current = getState();
3235
+ if (current && noiseImage) uploadNoise(current, noiseImage);
3236
+ invalidate();
3237
+ },
3238
+ () => {
3239
+ }
3240
+ );
3241
+ }
3242
+ return false;
3243
+ }
3244
+ function artSupported() {
3245
+ return getState() !== null;
3246
+ }
3247
+ function paintArt(target, job) {
3248
+ const s = getState();
3249
+ if (!s || s.lost) return false;
3250
+ if (job.variant === "grain" && !noiseReady(s)) return false;
3251
+ const entry = programFor(s, job.variant);
3252
+ if (!entry) return false;
3253
+ const { gl, canvas } = s;
3254
+ const { width, height } = job;
3255
+ if (canvas.width !== width || canvas.height !== height) {
3256
+ canvas.width = width;
3257
+ canvas.height = height;
3258
+ }
3259
+ gl.viewport(0, 0, width, height);
3260
+ gl.useProgram(entry.program);
3261
+ const at = (name) => {
3262
+ if (!entry.uniforms.has(name)) entry.uniforms.set(name, gl.getUniformLocation(entry.program, name));
3263
+ return entry.uniforms.get(name) ?? null;
3264
+ };
3265
+ const float = (name, value) => gl.uniform1f(at(name), value);
3266
+ gl.uniform2f(at("u_resolution"), width, height);
3267
+ float("u_pixelRatio", job.pixelRatio);
3268
+ float("u_originX", 0.5);
3269
+ float("u_originY", 0.5);
3270
+ float("u_worldWidth", 0);
3271
+ float("u_worldHeight", 0);
3272
+ float("u_fit", 2);
3273
+ float("u_scale", 1);
3274
+ float("u_offsetX", 0);
3275
+ float("u_offsetY", 0);
3276
+ const max = MAX_COLORS[job.variant];
3277
+ const fill = (colors) => {
3278
+ const flat = new Float32Array(max * 4);
3279
+ colors.slice(0, max).forEach((color, i) => flat.set(color, i * 4));
3280
+ gl.uniform4fv(at("u_colors"), flat);
3281
+ float("u_colorsCount", Math.min(colors.length, max));
3282
+ };
3283
+ if (job.variant === "mesh") {
3284
+ float("u_scale", 0.72);
3285
+ float("u_rotation", job.seed * 360);
3286
+ fill(job.colors);
3287
+ float("u_positions", job.seed * 100);
3288
+ float("u_waveX", job.warp);
3289
+ float("u_waveXShift", job.seed);
3290
+ float("u_waveY", job.warp);
3291
+ float("u_waveYShift", job.seed * 7.3 % 1);
3292
+ float("u_mixing", job.softness);
3293
+ float("u_grainMixer", job.grain * 0.6);
3294
+ float("u_grainOverlay", job.grain);
3295
+ } else {
3296
+ const [ground2 = [0, 0, 0, 1], ...bands] = job.colors;
3297
+ float("u_rotation", job.seed * 360);
3298
+ gl.uniform4fv(at("u_colorBack"), ground2);
3299
+ fill(bands.length ? bands : [ground2]);
3300
+ float("u_time", job.seed * 100);
3301
+ float("u_softness", job.softness * 0.8);
3302
+ float("u_intensity", 0.15 + job.warp * 0.2);
3303
+ float("u_noise", 0.15 + job.grain * 0.5);
3304
+ float("u_shape", GRAIN_CORNERS);
3305
+ gl.activeTexture(gl.TEXTURE0);
3306
+ gl.bindTexture(gl.TEXTURE_2D, s.noise);
3307
+ gl.uniform1i(at("u_noiseTexture"), 0);
3308
+ }
3309
+ gl.clearColor(0, 0, 0, 0);
3310
+ gl.clear(gl.COLOR_BUFFER_BIT);
3311
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
3312
+ if (target.width !== width || target.height !== height) {
3313
+ target.width = width;
3314
+ target.height = height;
3315
+ }
3316
+ const context = target.getContext("2d");
3317
+ if (!context) return false;
3318
+ context.clearRect(0, 0, width, height);
3319
+ context.drawImage(canvas, 0, 0);
3320
+ return true;
3321
+ }
3322
+ var swatch;
3323
+ var parsed = /* @__PURE__ */ new Map();
3324
+ function cssColorToRgba(color) {
3325
+ const hit = parsed.get(color);
3326
+ if (hit) return hit;
3327
+ if (swatch === void 0) {
3328
+ swatch = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d", { willReadFrequently: true });
3329
+ if (swatch) swatch.canvas.width = swatch.canvas.height = 1;
3330
+ }
3331
+ if (!swatch) return null;
3332
+ swatch.clearRect(0, 0, 1, 1);
3333
+ swatch.fillStyle = "transparent";
3334
+ swatch.fillStyle = color;
3335
+ swatch.fillRect(0, 0, 1, 1);
3336
+ const [r, g, b, a] = swatch.getImageData(0, 0, 1, 1).data;
3337
+ const out = [r / 255, g / 255, b / 255, a / 255];
3338
+ if (parsed.size > 512) parsed.clear();
3339
+ parsed.set(color, out);
3340
+ return out;
3341
+ }
3342
+ var themeListeners = /* @__PURE__ */ new Set();
3343
+ var observer = null;
3344
+ var pending = 0;
3345
+ function onThemeChange(listener) {
3346
+ themeListeners.add(listener);
3347
+ if (!observer && typeof MutationObserver !== "undefined") {
3348
+ const notify = () => {
3349
+ if (pending) return;
3350
+ pending = requestAnimationFrame(() => {
3351
+ pending = 0;
3352
+ for (const fn of themeListeners) fn();
3353
+ });
3354
+ };
3355
+ observer = new MutationObserver(notify);
3356
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style", "data-sable-theme"] });
3357
+ observer.observe(document.head, { childList: true, subtree: true, characterData: true });
3358
+ }
3359
+ return () => {
3360
+ themeListeners.delete(listener);
3361
+ if (!themeListeners.size && observer) {
3362
+ observer.disconnect();
3363
+ observer = null;
3364
+ cancelAnimationFrame(pending);
3365
+ pending = 0;
3366
+ }
3367
+ };
3368
+ }
3369
+ var ART_SLOTS = ["var(--art-1)", "var(--art-2)", "var(--art-3)", "var(--art-4)", "var(--art-5)", "var(--art-6)"];
3370
+ var ART_GROUND = "var(--art-ground)";
3371
+ var lifted = (tone) => `oklch(from var(--tone-${tone}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h)`;
3372
+ var ground = (tone) => `color-mix(in oklab, var(--primary) 55%, oklch(from var(--tone-${tone}) 0.3 c h))`;
3373
+ function seedValue(seed) {
3374
+ if (seed == null) return 0.37;
3375
+ if (typeof seed === "number") return Math.abs(seed * 0.618034) % 1;
3376
+ let hash = 2166136261;
3377
+ for (let i = 0; i < seed.length; i++) {
3378
+ hash ^= seed.charCodeAt(i);
3379
+ hash = Math.imul(hash, 16777619);
3380
+ }
3381
+ return (hash >>> 0) / 4294967296;
3382
+ }
3383
+ function fallback(palette, groundColor, seed) {
3384
+ const spots = palette.map((color, i) => {
3385
+ const angle = (seed + i / palette.length) * Math.PI * 2;
3386
+ const x = Math.round(50 + Math.cos(angle) * 38);
3387
+ const y = Math.round(50 + Math.sin(angle) * 38);
3388
+ return `radial-gradient(in oklab at ${x}% ${y}%, ${color}, transparent 60%)`;
3389
+ });
3390
+ return [...spots, groundColor].join(", ");
3391
+ }
3392
+ function rotate(items, seed) {
3393
+ if (items.length < 2) return [...items];
3394
+ const shift = Math.floor(seed * 7919) % items.length;
3395
+ return [...items.slice(shift), ...items.slice(0, shift)];
3396
+ }
3397
+ function param(style, name, fallbackValue) {
3398
+ const n = Number.parseFloat(style.getPropertyValue(name));
3399
+ return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : fallbackValue;
3400
+ }
3401
+ var MAX_PIXELS = 15e5;
3402
+ function GradientArt({ variant = "mesh", tones, seed, className, style, children, ...props }) {
3403
+ const root = useRef(null);
3404
+ const canvas = useRef(null);
3405
+ const probes = useRef(null);
3406
+ const value = seedValue(seed);
3407
+ const palette = tones?.length ? tones.map(lifted) : ART_SLOTS;
3408
+ const groundColor = tones?.length ? ground(tones[0]) : ART_GROUND;
3409
+ const paletteKey = palette.join(" ");
3410
+ const background = fallback(rotate(palette, value), groundColor, value);
3411
+ useEffect(() => {
3412
+ const element = root.current;
3413
+ const target = canvas.current;
3414
+ const swatches = probes.current;
3415
+ if (!element || !target || !swatches || !artSupported()) return;
3416
+ if (typeof IntersectionObserver === "undefined" || typeof ResizeObserver === "undefined") return;
3417
+ let visible = false;
3418
+ let frame = 0;
3419
+ let last = "";
3420
+ const paint = () => {
3421
+ frame = 0;
3422
+ if (!visible) return;
3423
+ const rect = element.getBoundingClientRect();
3424
+ if (!rect.width || !rect.height) return;
3425
+ let ratio = Math.min(window.devicePixelRatio || 1, 2);
3426
+ const pixels = rect.width * rect.height * ratio * ratio;
3427
+ if (pixels > MAX_PIXELS) ratio *= Math.sqrt(MAX_PIXELS / pixels);
3428
+ const width = Math.max(1, Math.round(rect.width * ratio));
3429
+ const height = Math.max(1, Math.round(rect.height * ratio));
3430
+ const resolve = (probe) => cssColorToRgba(getComputedStyle(probe).color);
3431
+ const [groundProbe, ...colorProbes] = Array.from(swatches.children);
3432
+ const colors = rotate(
3433
+ colorProbes.map(resolve).filter((c) => c != null && c[3] > 0.01),
3434
+ value
3435
+ );
3436
+ const groundRgba = groundProbe ? resolve(groundProbe) : null;
3437
+ if (!colors.length) return;
3438
+ const own = getComputedStyle(element);
3439
+ const character = { warp: param(own, "--art-warp", 0.8), softness: param(own, "--art-softness", 0.88), grain: param(own, "--art-grain", 0) };
3440
+ const key = JSON.stringify([variant, value, width, height, colors, groundRgba, character]);
3441
+ if (key === last) return;
3442
+ const ordered = variant === "grain" && groundRgba ? [groundRgba, ...colors] : colors;
3443
+ if (paintArt(target, { variant, colors: ordered, seed: value, width, height, pixelRatio: ratio, ...character })) {
3444
+ last = key;
3445
+ target.dataset.painted = "";
3446
+ }
3447
+ };
3448
+ const schedule = () => {
3449
+ if (!frame) frame = requestAnimationFrame(paint);
3450
+ };
3451
+ const repaint = () => {
3452
+ last = "";
3453
+ schedule();
3454
+ };
3455
+ const intersection = new IntersectionObserver(
3456
+ ([entry]) => {
3457
+ visible = entry.isIntersecting;
3458
+ if (visible) schedule();
3459
+ },
3460
+ { rootMargin: "200px" }
3461
+ );
3462
+ intersection.observe(element);
3463
+ const resize = new ResizeObserver(schedule);
3464
+ resize.observe(element);
3465
+ const offTheme = onThemeChange(schedule);
3466
+ const offInvalidate = onArtInvalidate(repaint);
3467
+ return () => {
3468
+ cancelAnimationFrame(frame);
3469
+ intersection.disconnect();
3470
+ resize.disconnect();
3471
+ offTheme();
3472
+ offInvalidate();
3473
+ };
3474
+ }, [variant, value, paletteKey]);
3475
+ return /* @__PURE__ */ jsxs(
3476
+ "div",
3477
+ {
3478
+ ref: root,
3479
+ "data-slot": "gradient-art",
3480
+ "data-variant": variant,
3481
+ className: cn("relative isolate overflow-hidden", className),
3482
+ style: { background, ...style },
3483
+ ...props,
3484
+ children: [
3485
+ /* @__PURE__ */ jsx(
3486
+ "canvas",
3487
+ {
3488
+ ref: canvas,
3489
+ "aria-hidden": true,
3490
+ className: "pointer-events-none absolute inset-0 -z-10 size-full opacity-0 motion-interactive data-[painted]:opacity-100",
3491
+ style: { transitionProperty: "opacity" }
3492
+ }
3493
+ ),
3494
+ /* @__PURE__ */ jsxs("span", { ref: probes, hidden: true, children: [
3495
+ /* @__PURE__ */ jsx("span", { style: { color: groundColor } }),
3496
+ palette.map((color) => /* @__PURE__ */ jsx("span", { style: { color } }, color))
3497
+ ] }),
3498
+ children
3499
+ ]
3500
+ }
3501
+ );
3502
+ }
3503
+ GradientArt.displayName = "GradientArt";
3504
+
3505
+ // src/components/gradient-art/gradient-art.meta.ts
3506
+ var gradientArtMeta = {
3507
+ name: "GradientArt",
3508
+ summary: "Placeholder artwork painted by a static WebGL shader from the preset's own art direction (the --art-* tokens: palette, ground, warp, softness, grain) \u2014 Ember burns, Prism airbrushes, Meridian is grainy monochrome \u2014 with a CSS gradient underneath for prerendered pages and browsers without WebGL2.",
3509
+ status: "beta",
3510
+ keywords: ["gradient", "mesh gradient", "grain", "shader", "artwork", "placeholder image", "cover", "banner", "avatar fallback", "generative"],
3511
+ usage: {
3512
+ description: "Use GradientArt where a real image belongs but none exists yet, or where the image is decoration: a product cover before upload, a profile banner, an album tile, an avatar with no photo. It reads the theme's own tokens, so it follows every preset, mode and live edit; `seed` fixes each item's composition so a list of covers or avatars stays individually recognisable.",
3513
+ bestPractices: [
3514
+ {
3515
+ guidance: "do",
3516
+ description: "Pass a stable `seed` per item \u2014 the name for an avatar, the title for a cover. Each item then keeps the same art across renders, reloads and themes, which is what lets someone recognise it."
3517
+ },
3518
+ {
3519
+ guidance: "do",
3520
+ description: "Size it with layout classes (`aspect-[4/3]`, `h-20`, `size-10 rounded-full`) and put any glyph or initials inside as children; they render above the art."
3521
+ },
3522
+ {
3523
+ guidance: "do",
3524
+ description: "Keep the default `mesh` for covers, tiles and avatars. Reach for `grain` where a printed, tactile surface suits \u2014 a banner, a hero card \u2014 and keep one variant per surface type so art reads as a system."
3525
+ },
3526
+ {
3527
+ guidance: "dont",
3528
+ description: "Do not put body text over it. The art spans the palette's lightest and most saturated tones, so no single ink is guaranteed to clear contrast across it; reserve overlays for a short label on a scrim or an avatar's initials."
3529
+ },
3530
+ {
3531
+ guidance: "dont",
3532
+ description: "Do not pass colours, and reach for `tones` rarely. The art follows the preset's --art-* tokens on purpose; to change how a product's art looks, set the --theme-art-* hooks in its theme or preset, so every cover, banner and avatar changes together."
3533
+ },
3534
+ {
3535
+ guidance: "dont",
3536
+ description: "Do not use it as a page or hero background. It paints at the element's pixel size, so a full-viewport instance costs a large frame on every theme change; it is sized for covers, banners and tiles."
3537
+ }
3538
+ ]
3539
+ },
3540
+ related: { components: ["Avatar", "Card", "AspectBox"] },
3541
+ props: [
3542
+ { name: "variant", type: '"mesh" | "grain"', default: '"mesh"', description: "mesh: soft spots of colour pulled together by gentle waves. grain: glowing light over deep corners with a printed texture." },
3543
+ {
3544
+ name: "tones",
3545
+ type: '("info" | "standard" | "collectible" | "edition" | "warning")[]',
3546
+ description: "Paint these `--tone-*` colours instead of the preset's art palette. Leave unset to follow the preset's art direction."
3547
+ },
3548
+ { name: "seed", type: "string | number", description: "Fixes the composition: the same seed paints the same art in every theme. Pass a stable id." },
3549
+ { name: "children", type: "ReactNode", description: "Rendered above the art \u2014 initials, a glyph, a short label." },
3550
+ { name: "className", type: "string", description: "Size and shape: aspect, height, rounding." }
3551
+ ],
3552
+ variants: { variant: ["mesh", "grain"] },
3553
+ tokensUsed: ["art-1", "art-2", "art-3", "art-4", "art-5", "art-6", "art-ground", "art-warp", "art-softness", "art-grain", "primary", "tone-info", "tone-standard", "tone-collectible", "tone-edition", "tone-warning"],
3554
+ motion: [
3555
+ {
3556
+ slot: "arrival",
3557
+ recipe: "motion-interactive",
3558
+ part: "canvas",
3559
+ why: "The shader frame fades in over the CSS gradient it replaces, so the swap from fallback to shader reads as the art settling rather than a flash."
3560
+ },
3561
+ { slot: "response", recipe: "none", part: "root", why: "Artwork, not a control: it does not react to the pointer." },
3562
+ {
3563
+ slot: "transition",
3564
+ recipe: "custom",
3565
+ part: "canvas",
3566
+ why: "A theme change repaints the frame in place on the next animation frame; there are no states to move between."
3567
+ },
3568
+ { slot: "activity", recipe: "none", part: "root", why: "Static by design: nothing animates between paints, so it costs nothing while idle and respects reduced motion without a branch." }
3569
+ ],
3570
+ a11y: [
3571
+ "Decorative by default: the canvas is aria-hidden. When the art stands for something (an avatar), label the element that contains it, not the art.",
3572
+ "Children render above the art in the normal reading order, so initials or a label are announced as usual."
3573
+ ],
3574
+ examples: [
3575
+ { label: "Product cover", code: '<GradientArt seed="Untitled (gold rush)" className="aspect-[4/3] rounded-lg" />' },
3576
+ { label: "Grain banner", code: '<GradientArt variant="grain" seed="juno" tones={["info", "edition"]} className="h-20 rounded-md" />' },
3577
+ {
3578
+ label: "Avatar with initials",
3579
+ code: '<GradientArt seed="Mara Okafor" className="grid size-10 place-items-center rounded-full text-label-md text-overlay-foreground">MO</GradientArt>'
3580
+ }
3581
+ ]
3582
+ };
3027
3583
  var avatarVariants = cva(
3028
3584
  "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-muted",
3029
3585
  {
@@ -3108,17 +3664,22 @@ AvatarFallback.displayName = "AvatarFallback";
3108
3664
  function Avatar({
3109
3665
  src,
3110
3666
  alt,
3111
- fallback,
3667
+ fallback: fallback2,
3112
3668
  fallbackDelay,
3113
3669
  size,
3114
3670
  status,
3671
+ variant = "neutral",
3672
+ seed,
3115
3673
  className,
3116
3674
  ...props
3117
3675
  }) {
3118
3676
  const named = !src && alt ? { role: "img", "aria-label": alt } : null;
3119
3677
  const avatar = /* @__PURE__ */ jsxs(AvatarRoot, { size, className: status == null ? className : void 0, ...named, ...props, children: [
3120
3678
  src ? /* @__PURE__ */ jsx(AvatarImage, { src, alt: alt ?? "" }) : null,
3121
- /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, children: fallback })
3679
+ variant === "art" ? /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, className: "size-full", children: /* @__PURE__ */ jsxs(GradientArt, { seed: seed ?? (typeof fallback2 === "string" && fallback2 || alt || void 0), className: "flex size-full items-center justify-center text-overlay-foreground", children: [
3680
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-0 bg-overlay/30" }),
3681
+ /* @__PURE__ */ jsx("span", { className: "relative", children: fallback2 })
3682
+ ] }) }) : /* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, children: fallback2 })
3122
3683
  ] });
3123
3684
  if (status == null) return avatar;
3124
3685
  return /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-status-anchor", className: cn("relative inline-flex shrink-0 rounded-full", className), children: [
@@ -3244,6 +3805,10 @@ var avatarMeta = {
3244
3805
  usage: {
3245
3806
  description: 'Use Avatar to represent a person, team, or account with a compact visual identity. Wrap several in AvatarGroup for collaborators, assignees, or attendees \u2014 it overlaps them, rings each in the surface colour, and collapses the rest into a "+N" count; use the low-level parts only when you need custom image/fallback layout.',
3246
3807
  bestPractices: [
3808
+ {
3809
+ guidance: "do",
3810
+ description: 'Use `variant="art"` where people without photos sit side by side \u2014 a member list, an activity feed, a chat. Seeded by the person, each gets their own colour, so faces stay tellable apart at a glance; the neutral fallback makes everyone the same grey circle.'
3811
+ },
3247
3812
  {
3248
3813
  guidance: "do",
3249
3814
  description: "Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person."
@@ -3338,6 +3903,17 @@ var avatarMeta = {
3338
3903
  type: '"sm" | "md" | "lg"',
3339
3904
  default: '"md"',
3340
3905
  description: "Square footprint: 32px / 40px / 48px."
3906
+ },
3907
+ {
3908
+ name: "variant",
3909
+ type: '"neutral" | "art"',
3910
+ default: '"neutral"',
3911
+ description: "What the fallback looks like. neutral: initials on the muted surface. art: initials over GradientArt seeded by the person, so each person without a photo gets their own stable colour from the theme."
3912
+ },
3913
+ {
3914
+ name: "seed",
3915
+ type: "string | number",
3916
+ description: "The art variant's seed. Defaults to the fallback text, then `alt`, so a person keeps one colour wherever they appear; pass a user id to tell apart two people who share initials."
3341
3917
  }
3342
3918
  ],
3343
3919
  subcomponents: [
@@ -3368,9 +3944,10 @@ var avatarMeta = {
3368
3944
  }
3369
3945
  ],
3370
3946
  variants: {
3371
- size: ["sm", "md", "lg"]
3947
+ size: ["sm", "md", "lg"],
3948
+ variant: ["neutral", "art"]
3372
3949
  },
3373
- tokensUsed: ["muted", "muted-foreground", "background", "card", "popover"],
3950
+ tokensUsed: ["muted", "muted-foreground", "background", "card", "popover", "overlay", "overlay-foreground"],
3374
3951
  a11y: [
3375
3952
  "Base UI tracks the image loading status and swaps to the fallback on missing/loading/error \u2014 no manual onError wiring.",
3376
3953
  "Provide meaningful alt text via the alt prop; pass an empty alt for decorative avatars.",
@@ -3387,6 +3964,10 @@ var avatarMeta = {
3387
3964
  label: "Fallback only",
3388
3965
  code: '<Avatar fallback="AM" alt="Alex Morgan" />'
3389
3966
  },
3967
+ {
3968
+ label: "Art fallback",
3969
+ code: '<Avatar variant="art" fallback="AM" alt="Alex Morgan" />'
3970
+ },
3390
3971
  {
3391
3972
  label: "Sizes",
3392
3973
  code: '<Avatar src="/pfp.png" alt="Alex Morgan" fallback="AM" size="sm" />\n<Avatar src="/pfp.png" alt="Alex Morgan" fallback="AM" size="md" />\n<Avatar src="/pfp.png" alt="Alex Morgan" fallback="AM" size="lg" />'
@@ -4117,9 +4698,11 @@ function MenuContent({
4117
4698
  }
4118
4699
  var MENU_ITEM_DESTRUCTIVE = (
4119
4700
  // The ink is pulled a quarter toward --foreground (as Badge's soft tones
4120
- // are): raw dark-mode destructive on the popover's 10% red wash measured
4121
- // 3.5-4.0:1 across themes. It reads as the same red and clears 4.5:1.
4122
- "text-[color-mix(in_oklch,var(--destructive)_75%,var(--foreground))] data-[highlighted]:bg-destructive/10 data-[highlighted]:text-[color-mix(in_oklch,var(--destructive)_75%,var(--foreground))]"
4701
+ // are), from --destructive-text rather than the fill: dark fills sit low
4702
+ // enough for a white label, and ink mixed from them measured 3.8:1 on
4703
+ // Meridian's popover. From the text token every theme clears 4.5:1 (worst
4704
+ // 5.4:1, Prism dark, on the 10% red wash).
4705
+ "text-[color-mix(in_oklch,var(--destructive-text)_75%,var(--foreground))] data-[highlighted]:bg-destructive/10 data-[highlighted]:text-[color-mix(in_oklch,var(--destructive-text)_75%,var(--foreground))]"
4123
4706
  );
4124
4707
  function MenuItem({ className, variant = "default", ...props }) {
4125
4708
  const tier = useTypeTier();
@@ -4819,6 +5402,29 @@ function useCopy(text, { timeout = 1500 } = {}) {
4819
5402
  }, [text, timeout]);
4820
5403
  return { copy, copied };
4821
5404
  }
5405
+ function useEdgeFade(ref, orientation) {
5406
+ const [showStart, setShowStart] = useState(false);
5407
+ const [showEnd, setShowEnd] = useState(false);
5408
+ const sync = useCallback(() => {
5409
+ const el = ref.current;
5410
+ if (!el) return;
5411
+ if (orientation === "horizontal") {
5412
+ const { scrollLeft, scrollWidth, clientWidth } = el;
5413
+ setShowStart(scrollLeft > 0);
5414
+ setShowEnd(scrollLeft < scrollWidth - clientWidth - 1);
5415
+ } else {
5416
+ const { scrollTop, scrollHeight, clientHeight } = el;
5417
+ setShowStart(scrollTop > 0);
5418
+ setShowEnd(scrollTop < scrollHeight - clientHeight - 1);
5419
+ }
5420
+ }, [ref, orientation]);
5421
+ useIsomorphicLayoutEffect(sync);
5422
+ useResizeObserver(sync, [ref]);
5423
+ const fade = "calc(var(--spacing) * 7)";
5424
+ const direction = orientation === "vertical" ? "bottom" : "right";
5425
+ const mask = showStart || showEnd ? `linear-gradient(to ${direction}, ${showStart ? "transparent" : "#000"}, #000 ${showStart ? fade : "0"}, #000 ${showEnd ? `calc(100% - ${fade})` : "100%"}, ${showEnd ? "transparent" : "#000"})` : void 0;
5426
+ return { sync, style: mask ? { maskImage: mask, WebkitMaskImage: mask } : void 0 };
5427
+ }
4822
5428
  var CodeHighlightContext = createContext(null);
4823
5429
  function CodeHighlightProvider({ highlight, children }) {
4824
5430
  return /* @__PURE__ */ jsx(CodeHighlightContext.Provider, { value: highlight, children });
@@ -4836,6 +5442,8 @@ function CodeBlock({
4836
5442
  const highlighter = highlight === false ? null : highlight ?? contextHighlight;
4837
5443
  const hasHeader = Boolean(filename);
4838
5444
  const { copy, copied } = useCopy(code);
5445
+ const preRef = useRef(null);
5446
+ const fade = useEdgeFade(preRef, "horizontal");
4839
5447
  if (prompt != null) {
4840
5448
  return /* @__PURE__ */ jsxs(
4841
5449
  "div",
@@ -4855,11 +5463,13 @@ function CodeBlock({
4855
5463
  }
4856
5464
  );
4857
5465
  }
5466
+ const copyColumn = !hasHeader && showCopy;
4858
5467
  return /* @__PURE__ */ jsxs(
4859
5468
  "div",
4860
5469
  {
4861
5470
  className: cn(
4862
- "relative overflow-hidden rounded-md border border-border bg-muted/40",
5471
+ "overflow-hidden rounded-md border border-border bg-muted/40",
5472
+ copyColumn && "flex items-start",
4863
5473
  className
4864
5474
  ),
4865
5475
  ...props,
@@ -4867,17 +5477,21 @@ function CodeBlock({
4867
5477
  hasHeader ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b border-border px-4 py-2", children: [
4868
5478
  /* @__PURE__ */ jsx("span", { className: "text-mono-caption text-muted-foreground", children: filename }),
4869
5479
  showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
4870
- ] }) : showCopy ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "absolute end-2 top-2 z-1 text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null,
5480
+ ] }) : null,
4871
5481
  /* @__PURE__ */ jsx(
4872
5482
  "pre",
4873
5483
  {
5484
+ ref: preRef,
5485
+ onScroll: fade.sync,
5486
+ style: fade.style,
4874
5487
  className: cn(
4875
5488
  "overflow-x-auto overscroll-x-contain p-4 text-mono-md text-foreground scrollbar-quiet",
4876
- !hasHeader && showCopy ? "pe-12" : ""
5489
+ copyColumn && "min-w-0 flex-1 pe-2"
4877
5490
  ),
4878
5491
  children: /* @__PURE__ */ jsx("code", { children: highlighter ? highlighter(code) : code })
4879
5492
  }
4880
- )
5493
+ ),
5494
+ copyColumn ? /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Copy code", onClick: copy, className: "m-2 ms-0 shrink-0 text-muted-foreground", children: /* @__PURE__ */ jsx(Icon, { name: copied ? "check" : "copy" }) }) : null
4881
5495
  ]
4882
5496
  }
4883
5497
  );
@@ -5600,7 +6214,7 @@ function DestructiveActionModal({
5600
6214
  confirmLabel = "Delete",
5601
6215
  cancelLabel = "Cancel",
5602
6216
  onConfirm,
5603
- pending,
6217
+ pending: pending2,
5604
6218
  confirmingLabel,
5605
6219
  children,
5606
6220
  confirmText
@@ -5609,8 +6223,8 @@ function DestructiveActionModal({
5609
6223
  const [internalBusy, setInternalBusy] = useState(false);
5610
6224
  const inFlight = useRef(false);
5611
6225
  const cancelRef = useRef(null);
5612
- const controlledPending = pending !== void 0;
5613
- const busy = pending ?? internalBusy;
6226
+ const controlledPending = pending2 !== void 0;
6227
+ const busy = pending2 ?? internalBusy;
5614
6228
  useEffect(() => {
5615
6229
  if (!open) {
5616
6230
  setTyped("");
@@ -6804,4 +7418,4 @@ var attachmentMeta = {
6804
7418
  ]
6805
7419
  };
6806
7420
 
6807
- export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, Attachment, Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, AvatarRoot, BADGE_APPEARANCES, BADGE_TONES, Badge, Bubble, BubbleReaction, BubbleReactions, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Code, CodeBlock, CodeHighlightProvider, Density, DensityProvider, DestructiveActionModal, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Entity, FIELD_INVALID, FIELD_SHELL, FIELD_SHELL_XS, Field, Form, HighlightGlide, Input, Label, MENU_ITEM_DESTRUCTIVE, MOBILE_FIELD_TIER, Menu, MenuCheckboxItem, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuPortal, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Message, MotionProvider, OverflowMenu, Pill, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue, Separator, SimpleTooltip, Spinner, StatusDot, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Textarea, Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, anchoredListScroll, anchoredScroll, attachmentMeta, avatarGroupVariants, avatarMeta, avatarVariants, badgeMeta, badgeVariants, bubbleMeta, buttonMeta, buttonVariants, cardMeta, codeBlockMeta, codeMeta, destructiveActionModalMeta, dialogMeta, entityMeta, formMeta, inputMeta, labelMeta, menuMeta, mergeRefs, messageMeta, overflowMenuMeta, pillVariants, progressMeta, progressTrackVariants, selectMeta, separatorMeta, setRef, spinnerMeta, statusDotMeta, statusDotVariants, switchMeta, tableMeta, textareaMeta, tooltipMeta, trimLabel, useAnchoredDensity, useCopy, useDensity, useIsomorphicLayoutEffect, useMotion, useReducedMotion, useResizeObserver, useScrollActivity, useTypeTier, withPortalThemeScope };
7421
+ export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogTitle, AlertDialogTrigger, Attachment, Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage, AvatarRoot, BADGE_APPEARANCES, BADGE_TONES, Badge, Bubble, BubbleReaction, BubbleReactions, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Code, CodeBlock, CodeHighlightProvider, Density, DensityProvider, DestructiveActionModal, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Entity, FIELD_INVALID, FIELD_SHELL, FIELD_SHELL_XS, Field, Form, GradientArt, HighlightGlide, Input, Label, MENU_ITEM_DESTRUCTIVE, MOBILE_FIELD_TIER, Menu, MenuCheckboxItem, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuPortal, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger, Message, MotionProvider, OverflowMenu, Pill, Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue, Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue, Separator, SimpleTooltip, Spinner, StatusDot, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Textarea, Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, anchoredListScroll, anchoredScroll, attachmentMeta, avatarGroupVariants, avatarMeta, avatarVariants, badgeMeta, badgeVariants, bubbleMeta, buttonMeta, buttonVariants, cardMeta, codeBlockMeta, codeMeta, destructiveActionModalMeta, dialogMeta, entityMeta, formMeta, gradientArtMeta, inputMeta, labelMeta, menuMeta, mergeRefs, messageMeta, overflowMenuMeta, pillVariants, progressMeta, progressTrackVariants, selectMeta, separatorMeta, setRef, spinnerMeta, statusDotMeta, statusDotVariants, switchMeta, tableMeta, textareaMeta, tooltipMeta, trimLabel, useAnchoredDensity, useCopy, useDensity, useEdgeFade, useIsomorphicLayoutEffect, useMotion, useReducedMotion, useResizeObserver, useScrollActivity, useTypeTier, withPortalThemeScope };