@sableui/react 0.5.0 → 0.6.1
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/AGENTS.md +2 -2
- package/CHANGELOG.md +148 -0
- package/CONTRIBUTING.md +1 -1
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/bin/sable-lint.mjs +2 -1
- package/bin/sable.mjs +3 -2
- package/dist/{_contract-Cn3lUpih.d.ts → _contract-C2M1SjBf.d.ts} +20 -2
- package/dist/blocks/index.js +19 -51
- package/dist/{chunk-RCLKXF2N.js → chunk-7QSDYBXO.js} +610 -30
- package/dist/{chunk-GU3T7SBA.js → chunk-EDXLM7YL.js} +1 -1
- package/dist/chunk-FKZXAFIY.js +12 -0
- package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
- package/dist/{chunk-ZBZMKOO7.js → chunk-VVW4UCDI.js} +3 -3
- package/dist/foundations/index.js +6 -0
- package/dist/index.d.ts +127 -11
- package/dist/index.js +23 -13
- package/dist/ios/index.d.ts +6 -1
- package/dist/ios/index.js +4 -3
- package/dist/ios/styles.css +9 -0
- package/dist/patterns/index.js +22 -28
- package/dist/presets/index.d.ts +108 -5
- package/dist/presets/index.js +239 -6
- package/dist/styles.css +50 -0
- package/dist/{theme-provider-CGeiusgu.d.ts → theme-provider-kQszHYji.d.ts} +1 -1
- package/dist/themes/atelier/atelier.css +31 -3
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +38 -7
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +33 -4
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +28 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +26 -0
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +24 -11
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +34 -4
- package/llms/blocks/danger-zone-block.md +1 -1
- package/llms/blocks/event-card-block.md +1 -1
- package/llms/blocks/feedback-form-block.md +1 -1
- package/llms/blocks/invite-team-block.md +1 -1
- package/llms/blocks/media-player-block.md +3 -16
- package/llms/blocks/notification-list-block.md +3 -3
- package/llms/blocks/product-card-block.md +3 -17
- package/llms/blocks/profile-card-block.md +4 -17
- package/llms/blocks/profile-form-block.md +2 -2
- package/llms/blocks/team-members-block.md +1 -1
- package/llms/blocks/testimonial-block.md +1 -1
- package/llms/components/avatar.md +13 -1
- package/llms/components/card.md +1 -1
- package/llms/components/gradient-art.md +80 -0
- package/llms/components/sheet.md +1 -1
- package/llms/foundations/accessibility.md +1 -0
- package/llms/patterns/auth-page.md +5 -19
- package/llms/patterns/brand-landing-page.md +1 -1
- package/llms/patterns/chat-panel.md +1 -1
- package/llms/patterns/dashboard-page.md +4 -4
- package/llms/patterns/feed-page.md +4 -4
- package/llms/patterns/landing-page.md +1 -1
- package/llms/patterns/list-detail-page.md +3 -2
- package/llms/patterns/settings-page.md +3 -3
- package/llms/reference/token-intent.md +9 -1
- package/llms/tasks/auditing-an-app.md +6 -0
- package/llms/tasks/building-an-ios-prototype.md +3 -1
- package/llms/tasks/converting-an-app.md +1 -1
- package/llms/tasks/creating-a-preset.md +24 -0
- package/llms/tasks/reviewing-a-design.md +9 -0
- package/llms/tasks/using-presets.md +6 -1
- package/llms-full.txt +201 -104
- package/llms.txt +3 -2
- package/package.json +9 -7
- package/registry.json +283 -30
- package/scripts/design-presets.mjs +27 -4
- package/scripts/grade.mjs +2 -1
- package/scripts/label-contrast.mjs +174 -0
- package/scripts/lint-rules.mjs +39 -2
- package/skills/sable/SKILL.md +3 -1
- package/targets/ios.json +18 -2
- package/third-party/paper-shaders-LICENSE.txt +201 -0
- package/third-party/paper-shaders-NOTICE.txt +5 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StackedActionsContext, CardDepthContext, usePresentation, PresentationProvider } from './chunk-FKZXAFIY.js';
|
|
1
2
|
import { cn } from './chunk-SRQWQUTW.js';
|
|
2
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';
|
|
@@ -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
|
|
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
|
-
|
|
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: {
|
|
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({
|
|
@@ -924,20 +935,27 @@ function CardList({
|
|
|
924
935
|
);
|
|
925
936
|
}
|
|
926
937
|
function CardFooter({
|
|
938
|
+
actions = false,
|
|
927
939
|
className,
|
|
940
|
+
children,
|
|
928
941
|
...props
|
|
929
942
|
}) {
|
|
943
|
+
const depth = useContext(CardDepthContext);
|
|
944
|
+
const stacked = usePresentation() === "sheet" && actions && depth <= 1;
|
|
930
945
|
return /* @__PURE__ */ jsx(
|
|
931
946
|
"div",
|
|
932
947
|
{
|
|
933
948
|
"data-slot": "card-footer",
|
|
949
|
+
"data-actions": actions ? stacked ? "stack" : "row" : void 0,
|
|
934
950
|
className: cn(
|
|
935
951
|
"flex items-center gap-2 p-[var(--card-p,--spacing(6))] pt-0",
|
|
936
952
|
"[[data-slot=card-header]+&]:pt-[var(--card-p,--spacing(6))]",
|
|
937
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))]",
|
|
938
|
-
className
|
|
954
|
+
className,
|
|
955
|
+
stacked && "mt-auto flex-col-reverse items-stretch justify-start"
|
|
939
956
|
),
|
|
940
|
-
...props
|
|
957
|
+
...props,
|
|
958
|
+
children: stacked ? /* @__PURE__ */ jsx(StackedActionsContext.Provider, { value: true, children }) : children
|
|
941
959
|
}
|
|
942
960
|
);
|
|
943
961
|
}
|
|
@@ -1072,8 +1090,8 @@ var cardMeta = {
|
|
|
1072
1090
|
},
|
|
1073
1091
|
{
|
|
1074
1092
|
name: "CardFooter",
|
|
1075
|
-
type: 'ComponentPropsWithRef<"div">',
|
|
1076
|
-
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."
|
|
1077
1095
|
}
|
|
1078
1096
|
],
|
|
1079
1097
|
variants: {
|
|
@@ -1325,16 +1343,18 @@ var MATRIX = {
|
|
|
1325
1343
|
// raw tone clears 4.5:1 on the page, but on its own 15% wash it did not
|
|
1326
1344
|
// (light info 4.2:1, dark edition 3.6:1, most of Verdant's tones ~4.1:1).
|
|
1327
1345
|
// Mixing toward --foreground moves it AWAY from the wash in both modes and
|
|
1328
|
-
// 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).
|
|
1329
1349
|
soft: {
|
|
1330
1350
|
neutral: "bg-secondary text-secondary-foreground",
|
|
1331
1351
|
standard: "bg-tone-standard/15 text-[color-mix(in_oklch,var(--tone-standard)_75%,var(--foreground))]",
|
|
1332
1352
|
collectible: "bg-tone-collectible/15 text-[color-mix(in_oklch,var(--tone-collectible)_75%,var(--foreground))]",
|
|
1333
1353
|
edition: "bg-tone-edition/15 text-[color-mix(in_oklch,var(--tone-edition)_75%,var(--foreground))]",
|
|
1334
|
-
info: "bg-tone-info/15 text-[color-mix(in_oklch,var(--tone-info)_75%,var(--foreground))]",
|
|
1335
|
-
success: "bg-success/15 text-[color-mix(in_oklch,var(--success)_75%,var(--foreground))]",
|
|
1336
|
-
warning: "bg-tone-warning/15 text-[color-mix(in_oklch,var(--tone-warning)_75%,var(--foreground))]",
|
|
1337
|
-
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))]"
|
|
1338
1358
|
},
|
|
1339
1359
|
outline: {
|
|
1340
1360
|
neutral: "border border-border text-foreground",
|
|
@@ -1639,16 +1659,16 @@ function usePortalThemeScope() {
|
|
|
1639
1659
|
const [portal, setPortal] = useState(null);
|
|
1640
1660
|
const [probed, setProbed] = useState(null);
|
|
1641
1661
|
const probeRef = useRef(null);
|
|
1642
|
-
const
|
|
1662
|
+
const pending2 = portal !== null && probed !== portal;
|
|
1643
1663
|
useIsomorphicLayoutEffect(() => {
|
|
1644
|
-
if (!
|
|
1664
|
+
if (!pending2 || !portal) return;
|
|
1645
1665
|
const probe = probeRef.current;
|
|
1646
1666
|
if (probe) applyPortalThemeScope(portal, resolvePortalThemeScope(probe));
|
|
1647
1667
|
setProbed(portal);
|
|
1648
|
-
}, [
|
|
1668
|
+
}, [pending2, portal]);
|
|
1649
1669
|
return {
|
|
1650
1670
|
portalRef: setPortal,
|
|
1651
|
-
probe:
|
|
1671
|
+
probe: pending2 ? /* @__PURE__ */ jsx("span", { ref: probeRef, hidden: true, "data-sable-portal-probe": "" }) : null
|
|
1652
1672
|
};
|
|
1653
1673
|
}
|
|
1654
1674
|
function withPortalThemeScope(Portal) {
|
|
@@ -1720,7 +1740,7 @@ function FullscreenContent({
|
|
|
1720
1740
|
),
|
|
1721
1741
|
...props,
|
|
1722
1742
|
children: [
|
|
1723
|
-
children,
|
|
1743
|
+
/* @__PURE__ */ jsx(PresentationProvider, { value: "page", children }),
|
|
1724
1744
|
showCloseButton ? (
|
|
1725
1745
|
// The floating Button: Sable's control over content. Its own
|
|
1726
1746
|
// translucent popover chip and border keep it legible over bright
|
|
@@ -1778,7 +1798,7 @@ function CardContent2({
|
|
|
1778
1798
|
),
|
|
1779
1799
|
...props,
|
|
1780
1800
|
children: [
|
|
1781
|
-
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 }),
|
|
1782
1802
|
showCloseButton ? /* @__PURE__ */ jsx(
|
|
1783
1803
|
Dialog$1.Close,
|
|
1784
1804
|
{
|
|
@@ -3027,6 +3047,539 @@ var switchMeta = {
|
|
|
3027
3047
|
}
|
|
3028
3048
|
]
|
|
3029
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
|
+
};
|
|
3030
3583
|
var avatarVariants = cva(
|
|
3031
3584
|
"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-muted",
|
|
3032
3585
|
{
|
|
@@ -3111,17 +3664,22 @@ AvatarFallback.displayName = "AvatarFallback";
|
|
|
3111
3664
|
function Avatar({
|
|
3112
3665
|
src,
|
|
3113
3666
|
alt,
|
|
3114
|
-
fallback,
|
|
3667
|
+
fallback: fallback2,
|
|
3115
3668
|
fallbackDelay,
|
|
3116
3669
|
size,
|
|
3117
3670
|
status,
|
|
3671
|
+
variant = "neutral",
|
|
3672
|
+
seed,
|
|
3118
3673
|
className,
|
|
3119
3674
|
...props
|
|
3120
3675
|
}) {
|
|
3121
3676
|
const named = !src && alt ? { role: "img", "aria-label": alt } : null;
|
|
3122
3677
|
const avatar = /* @__PURE__ */ jsxs(AvatarRoot, { size, className: status == null ? className : void 0, ...named, ...props, children: [
|
|
3123
3678
|
src ? /* @__PURE__ */ jsx(AvatarImage, { src, alt: alt ?? "" }) : null,
|
|
3124
|
-
/* @__PURE__ */ jsx(AvatarFallback, { delay: fallbackDelay, "aria-hidden": named ? true : void 0, children:
|
|
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 })
|
|
3125
3683
|
] });
|
|
3126
3684
|
if (status == null) return avatar;
|
|
3127
3685
|
return /* @__PURE__ */ jsxs("span", { "data-slot": "avatar-status-anchor", className: cn("relative inline-flex shrink-0 rounded-full", className), children: [
|
|
@@ -3247,6 +3805,10 @@ var avatarMeta = {
|
|
|
3247
3805
|
usage: {
|
|
3248
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.',
|
|
3249
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
|
+
},
|
|
3250
3812
|
{
|
|
3251
3813
|
guidance: "do",
|
|
3252
3814
|
description: "Provide meaningful alt text for profile images, or an empty alt when the adjacent text already names the person."
|
|
@@ -3341,6 +3903,17 @@ var avatarMeta = {
|
|
|
3341
3903
|
type: '"sm" | "md" | "lg"',
|
|
3342
3904
|
default: '"md"',
|
|
3343
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."
|
|
3344
3917
|
}
|
|
3345
3918
|
],
|
|
3346
3919
|
subcomponents: [
|
|
@@ -3371,9 +3944,10 @@ var avatarMeta = {
|
|
|
3371
3944
|
}
|
|
3372
3945
|
],
|
|
3373
3946
|
variants: {
|
|
3374
|
-
size: ["sm", "md", "lg"]
|
|
3947
|
+
size: ["sm", "md", "lg"],
|
|
3948
|
+
variant: ["neutral", "art"]
|
|
3375
3949
|
},
|
|
3376
|
-
tokensUsed: ["muted", "muted-foreground", "background", "card", "popover"],
|
|
3950
|
+
tokensUsed: ["muted", "muted-foreground", "background", "card", "popover", "overlay", "overlay-foreground"],
|
|
3377
3951
|
a11y: [
|
|
3378
3952
|
"Base UI tracks the image loading status and swaps to the fallback on missing/loading/error \u2014 no manual onError wiring.",
|
|
3379
3953
|
"Provide meaningful alt text via the alt prop; pass an empty alt for decorative avatars.",
|
|
@@ -3390,6 +3964,10 @@ var avatarMeta = {
|
|
|
3390
3964
|
label: "Fallback only",
|
|
3391
3965
|
code: '<Avatar fallback="AM" alt="Alex Morgan" />'
|
|
3392
3966
|
},
|
|
3967
|
+
{
|
|
3968
|
+
label: "Art fallback",
|
|
3969
|
+
code: '<Avatar variant="art" fallback="AM" alt="Alex Morgan" />'
|
|
3970
|
+
},
|
|
3393
3971
|
{
|
|
3394
3972
|
label: "Sizes",
|
|
3395
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" />'
|
|
@@ -4120,9 +4698,11 @@ function MenuContent({
|
|
|
4120
4698
|
}
|
|
4121
4699
|
var MENU_ITEM_DESTRUCTIVE = (
|
|
4122
4700
|
// The ink is pulled a quarter toward --foreground (as Badge's soft tones
|
|
4123
|
-
// are)
|
|
4124
|
-
//
|
|
4125
|
-
|
|
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))]"
|
|
4126
4706
|
);
|
|
4127
4707
|
function MenuItem({ className, variant = "default", ...props }) {
|
|
4128
4708
|
const tier = useTypeTier();
|
|
@@ -5634,7 +6214,7 @@ function DestructiveActionModal({
|
|
|
5634
6214
|
confirmLabel = "Delete",
|
|
5635
6215
|
cancelLabel = "Cancel",
|
|
5636
6216
|
onConfirm,
|
|
5637
|
-
pending,
|
|
6217
|
+
pending: pending2,
|
|
5638
6218
|
confirmingLabel,
|
|
5639
6219
|
children,
|
|
5640
6220
|
confirmText
|
|
@@ -5643,8 +6223,8 @@ function DestructiveActionModal({
|
|
|
5643
6223
|
const [internalBusy, setInternalBusy] = useState(false);
|
|
5644
6224
|
const inFlight = useRef(false);
|
|
5645
6225
|
const cancelRef = useRef(null);
|
|
5646
|
-
const controlledPending =
|
|
5647
|
-
const busy =
|
|
6226
|
+
const controlledPending = pending2 !== void 0;
|
|
6227
|
+
const busy = pending2 ?? internalBusy;
|
|
5648
6228
|
useEffect(() => {
|
|
5649
6229
|
if (!open) {
|
|
5650
6230
|
setTyped("");
|
|
@@ -6838,4 +7418,4 @@ var attachmentMeta = {
|
|
|
6838
7418
|
]
|
|
6839
7419
|
};
|
|
6840
7420
|
|
|
6841
|
-
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, useEdgeFade, 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 };
|