@dashforge/tw 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -11813,6 +11813,665 @@ Avatar.displayName = 'Avatar';
11813
11813
  });
11814
11814
  AvatarGroup.displayName = 'AvatarGroup';
11815
11815
 
11816
+ /**
11817
+ * `<Skeleton>` variant recipe.
11818
+ *
11819
+ * Slots:
11820
+ * - `root` — single element (atomic component)
11821
+ *
11822
+ * Variant axes:
11823
+ * - `variant` (`text` | `rectangle` | `circle`) — shape / radius
11824
+ * - `animation` (`pulse` | `wave` | `none`) — how the placeholder
11825
+ * "breathes" while data loads. Always gated on
11826
+ * `prefers-reduced-motion` (no exception — animations are
11827
+ * purely cosmetic).
11828
+ *
11829
+ * Implementation notes:
11830
+ *
11831
+ * - `pulse` uses Tailwind's built-in `animate-pulse` (opacity
11832
+ * 1.0 ↔ 0.5 over 2s, ease-in-out infinite).
11833
+ * - `wave` overlays a moving gradient. Implemented with a
11834
+ * `relative` container + an `::after` pseudo-element via a
11835
+ * Tailwind arbitrary-value class. The gradient slides
11836
+ * horizontally over the surface, mimicking the MUI Skeleton
11837
+ * "wave" look.
11838
+ * - Color uses neutral-200 (light) and neutral-800 (dark) for
11839
+ * the base, picked up automatically by the dashforge preset's
11840
+ * CSS-variable layer.
11841
+ *
11842
+ * Accessibility: the root has `aria-hidden="true"` (set on the JSX
11843
+ * side, not here). Screen readers skip the skeleton entirely;
11844
+ * the surrounding component is responsible for announcing
11845
+ * loading state via `aria-busy` or `aria-live` if needed.
11846
+ */ const skeletonVariants = tv({
11847
+ slots: {
11848
+ root: [
11849
+ 'block',
11850
+ // Neutral palette auto-inverts via CSS-var swap (no `dark:`).
11851
+ 'bg-neutral-200',
11852
+ 'motion-reduce:animate-none'
11853
+ ]
11854
+ },
11855
+ variants: {
11856
+ variant: {
11857
+ text: {
11858
+ root: 'rounded h-[1em] w-full'
11859
+ },
11860
+ rectangle: {
11861
+ root: 'rounded-md w-full h-[100px]'
11862
+ },
11863
+ circle: {
11864
+ root: 'rounded-full w-10 h-10'
11865
+ }
11866
+ },
11867
+ animation: {
11868
+ pulse: {
11869
+ root: 'animate-pulse'
11870
+ },
11871
+ wave: {
11872
+ root: [
11873
+ 'relative overflow-hidden',
11874
+ // The wave is rendered via an arbitrary-value ::after
11875
+ // pseudo-element with a moving linear gradient. Tailwind
11876
+ // doesn't ship a `wave` keyframe; we use the built-in
11877
+ // `animate-pulse` as a fallback if a consumer needs to
11878
+ // override. The gradient stop direction works in both
11879
+ // light and dark mode (white shimmer in light, lighter
11880
+ // overlay in dark).
11881
+ 'after:absolute after:inset-0',
11882
+ 'after:bg-gradient-to-r after:from-transparent after:via-white/40 after:to-transparent',
11883
+ 'dark:after:via-white/10',
11884
+ 'after:animate-pulse',
11885
+ 'after:motion-reduce:animate-none'
11886
+ ]
11887
+ },
11888
+ none: {
11889
+ root: ''
11890
+ }
11891
+ }
11892
+ },
11893
+ defaultVariants: {
11894
+ variant: 'text',
11895
+ animation: 'pulse'
11896
+ }
11897
+ });
11898
+
11899
+ /**
11900
+ * Dashforge TW `<Skeleton>` — loading placeholder primitive.
11901
+ *
11902
+ * Renders as a non-interactive `<span aria-hidden="true">` so screen
11903
+ * readers skip it. The surrounding component is responsible for
11904
+ * announcing loading state via `aria-busy` or `aria-live` if needed.
11905
+ *
11906
+ * Variants:
11907
+ * - `variant` — `text` (default, line shape) / `rectangle` / `circle`
11908
+ * - `animation` — `pulse` (default) / `wave` / `none`
11909
+ *
11910
+ * Both animations are gated by `prefers-reduced-motion` (WCAG 2.3.3).
11911
+ *
11912
+ * @example
11913
+ * ```tsx
11914
+ * <Skeleton variant="text" width="200px" />
11915
+ * <Skeleton variant="rectangle" width="100%" height="120px" />
11916
+ * <Skeleton variant="circle" width="40px" />
11917
+ * ```
11918
+ */ function Skeleton(props) {
11919
+ var _themeSlotProps_root, _slotProps_root;
11920
+ const themeDefaults = useComponentDefaults('Skeleton');
11921
+ const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
11922
+ const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
11923
+ const { variant = 'text', animation = 'pulse', width, height, sx, slotProps } = merged;
11924
+ const v = skeletonVariants({
11925
+ variant,
11926
+ animation
11927
+ });
11928
+ // Width / height come through as inline style — Tailwind doesn't
11929
+ // know about arbitrary user-supplied lengths. We default `circle`'s
11930
+ // height to `width` so consumers don't have to repeat themselves.
11931
+ const style = {};
11932
+ if (width != null) style.width = width;
11933
+ if (height != null) {
11934
+ style.height = height;
11935
+ } else if (variant === 'circle' && width != null) {
11936
+ style.height = width;
11937
+ }
11938
+ return jsx("span", {
11939
+ "aria-hidden": "true",
11940
+ role: "presentation",
11941
+ className: cn(v.root(), sx, themeSlotProps == null ? void 0 : (_themeSlotProps_root = themeSlotProps.root) == null ? void 0 : _themeSlotProps_root.className, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
11942
+ style: style
11943
+ });
11944
+ }
11945
+
11946
+ /**
11947
+ * `<Image>` variant recipe.
11948
+ *
11949
+ * Slots:
11950
+ * - `root` — positioned wrapper. Holds the `<img>` plus the
11951
+ * loading skeleton / error-fallback overlays, and owns
11952
+ * the corner radius + overflow clipping. `relative` so
11953
+ * the overlays can be `absolute inset-0`.
11954
+ * - `img` — the `<img>` element. `object-*` (the `fit` axis)
11955
+ * lives here; `block w-full h-full` so it fills the
11956
+ * reserved box when an aspect ratio / dimensions are
11957
+ * given.
11958
+ * - `skeleton` — the loading placeholder overlay (a `<Skeleton>`
11959
+ * stretched over the reserved box).
11960
+ * - `fallback` — the broken-image overlay shown after `onError`.
11961
+ *
11962
+ * Variant axes:
11963
+ * - `fit` — CSS `object-fit`. `cover` (default) crops to fill,
11964
+ * `contain` letterboxes, etc.
11965
+ * - `rounded` — corner radius token, applied to `root` (which clips
11966
+ * the image via `overflow-hidden`).
11967
+ *
11968
+ * Neutral fallback surface auto-inverts through the dashforge preset's
11969
+ * CSS-variable layer (no explicit `dark:`).
11970
+ */ const imageVariants = tv({
11971
+ slots: {
11972
+ root: [
11973
+ 'relative',
11974
+ 'block',
11975
+ 'overflow-hidden'
11976
+ ],
11977
+ img: [
11978
+ 'block',
11979
+ 'w-full',
11980
+ 'h-full'
11981
+ ],
11982
+ skeleton: [
11983
+ 'absolute',
11984
+ 'inset-0',
11985
+ 'h-full',
11986
+ 'w-full'
11987
+ ],
11988
+ fallback: [
11989
+ 'absolute',
11990
+ 'inset-0',
11991
+ 'flex',
11992
+ 'items-center',
11993
+ 'justify-center',
11994
+ // Neutral surface — auto-inverts via CSS vars, no `dark:`.
11995
+ 'bg-neutral-100',
11996
+ 'text-neutral-400'
11997
+ ]
11998
+ },
11999
+ variants: {
12000
+ fit: {
12001
+ cover: {
12002
+ img: 'object-cover'
12003
+ },
12004
+ contain: {
12005
+ img: 'object-contain'
12006
+ },
12007
+ fill: {
12008
+ img: 'object-fill'
12009
+ },
12010
+ none: {
12011
+ img: 'object-none'
12012
+ },
12013
+ 'scale-down': {
12014
+ img: 'object-scale-down'
12015
+ }
12016
+ },
12017
+ rounded: {
12018
+ none: {
12019
+ root: 'rounded-none'
12020
+ },
12021
+ sm: {
12022
+ root: 'rounded-sm'
12023
+ },
12024
+ md: {
12025
+ root: 'rounded-md'
12026
+ },
12027
+ lg: {
12028
+ root: 'rounded-lg'
12029
+ },
12030
+ full: {
12031
+ root: 'rounded-full'
12032
+ }
12033
+ }
12034
+ },
12035
+ defaultVariants: {
12036
+ fit: 'cover',
12037
+ rounded: 'none'
12038
+ }
12039
+ });
12040
+
12041
+ /**
12042
+ * Broken-image glyph — inline SVG fallback shown after a load error
12043
+ * when no custom `fallback` is provided. Inline (no icon-library dep),
12044
+ * mirroring `<Avatar>`'s default user icon.
12045
+ *
12046
+ * @internal
12047
+ */ function BrokenImageIcon() {
12048
+ return jsxs("svg", {
12049
+ width: "24",
12050
+ height: "24",
12051
+ viewBox: "0 0 24 24",
12052
+ fill: "none",
12053
+ stroke: "currentColor",
12054
+ strokeWidth: "1.6",
12055
+ strokeLinecap: "round",
12056
+ strokeLinejoin: "round",
12057
+ "aria-hidden": "true",
12058
+ children: [
12059
+ jsx("rect", {
12060
+ x: "3",
12061
+ y: "3",
12062
+ width: "18",
12063
+ height: "18",
12064
+ rx: "2"
12065
+ }),
12066
+ jsx("path", {
12067
+ d: "m3 15 5-5 4 4"
12068
+ }),
12069
+ jsx("path", {
12070
+ d: "m14 14 2-2 5 5"
12071
+ }),
12072
+ jsx("path", {
12073
+ d: "m21 3-18 18"
12074
+ })
12075
+ ]
12076
+ });
12077
+ }
12078
+ /**
12079
+ * Dashforge TW `<Image>` — a declarative, robust wrapper over the
12080
+ * native `<img>`. Zero eval, no data fetching: it renders a URL you
12081
+ * give it, and adds the three things a bare `<img>` lacks.
12082
+ *
12083
+ * 1. **No layout shift** — pass `aspectRatio` (or `width` + `height`)
12084
+ * and the box is reserved before the image loads (CSS
12085
+ * `aspect-ratio`, no JS measurement).
12086
+ * 2. **Loading skeleton** — a `<Skeleton>` fills the reserved box
12087
+ * while the image loads, and is skipped for already-cached images
12088
+ * so there is no flash.
12089
+ * 3. **Graceful error** — on load failure a muted fallback replaces
12090
+ * the broken-image glyph (`fallback` to customise).
12091
+ *
12092
+ * Lazy by default (`loading="lazy"`). Any native `<img>` attribute not
12093
+ * named here (`srcSet`, `sizes`, `decoding`, …) is forwarded to the
12094
+ * underlying element. The ref points at the `<img>`.
12095
+ *
12096
+ * @example
12097
+ * ```tsx
12098
+ * <Image src="/hero.jpg" alt="Team at work" aspectRatio={16 / 9} rounded="lg" />
12099
+ * <Image src={avatarUrl} alt="Jane Doe" width={48} height={48} rounded="full" />
12100
+ * ```
12101
+ */ const Image = /*#__PURE__*/ forwardRef(function Image(props, ref) {
12102
+ var _themeSlotProps_root, _slotProps_root, _themeSlotProps_img, _slotProps_img, _themeSlotProps_skeleton, _slotProps_skeleton, _themeSlotProps_fallback, _slotProps_fallback;
12103
+ const themeDefaults = useComponentDefaults('Image');
12104
+ const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
12105
+ const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
12106
+ const { src, alt, aspectRatio, width, height, fit = 'cover', rounded = 'none', loading = 'lazy', showSkeleton = true, fallback, access, visibleWhen, sx, slotProps, onLoad, onError, style } = merged, rest = _object_without_properties_loose(merged, [
12107
+ "src",
12108
+ "alt",
12109
+ "aspectRatio",
12110
+ "width",
12111
+ "height",
12112
+ "fit",
12113
+ "rounded",
12114
+ "loading",
12115
+ "showSkeleton",
12116
+ "fallback",
12117
+ "access",
12118
+ "visibleWhen",
12119
+ "sx",
12120
+ "slotProps",
12121
+ "onLoad",
12122
+ "onError",
12123
+ "style"
12124
+ ]);
12125
+ // Gating — hooks are always called (rules of hooks), the early return
12126
+ // comes after. `visibleWhen` is a no-op outside a <DashForm>.
12127
+ const bridge = useContext(DashFormContext);
12128
+ const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
12129
+ const accessState = useAccessState(access);
12130
+ const [loaded, setLoaded] = useState(false);
12131
+ const [failed, setFailed] = useState(false);
12132
+ // Reset load / error state when the source changes in place — React's
12133
+ // official "adjust state during render" pattern (no effect, no flash),
12134
+ // so a previously-failed image doesn't keep showing its fallback.
12135
+ const [prevSrc, setPrevSrc] = useState(src);
12136
+ if (src !== prevSrc) {
12137
+ setPrevSrc(src);
12138
+ setLoaded(false);
12139
+ setFailed(false);
12140
+ }
12141
+ // Forward the ref AND resolve the already-cached case: for a cached
12142
+ // image `onLoad` may fire before React attaches the handler, so we
12143
+ // read `.complete` at mount and skip the skeleton flash.
12144
+ const handleRef = useCallback((node)=>{
12145
+ if (typeof ref === 'function') ref(node);
12146
+ else if (ref) ref.current = node;
12147
+ if (node && node.complete && node.naturalWidth > 0) setLoaded(true);
12148
+ }, [
12149
+ ref
12150
+ ]);
12151
+ const handleLoad = (e)=>{
12152
+ setLoaded(true);
12153
+ onLoad == null ? void 0 : onLoad(e);
12154
+ };
12155
+ const handleError = (e)=>{
12156
+ setFailed(true);
12157
+ onError == null ? void 0 : onError(e);
12158
+ };
12159
+ // Gated out entirely (hooks above already ran — rules of hooks safe).
12160
+ if (!isVisible || !accessState.visible) return null;
12161
+ // An image has no interactive state, so 'disable' / 'readonly' render it
12162
+ // dimmed and non-interactive rather than truly disabled.
12163
+ const accessDimmed = accessState.disabled || accessState.readonly;
12164
+ const v = imageVariants({
12165
+ fit,
12166
+ rounded
12167
+ });
12168
+ // A skeleton needs a reserved box to paint into — without a known
12169
+ // shape there is no space, so we skip it (and there's layout shift,
12170
+ // documented as the reason to always pass aspectRatio / dimensions).
12171
+ const hasReservedBox = aspectRatio != null || width != null && height != null;
12172
+ const showSkeletonOverlay = showSkeleton && !loaded && !failed && hasReservedBox;
12173
+ const rootStyle = _extends({}, style);
12174
+ if (aspectRatio != null) {
12175
+ rootStyle.aspectRatio = typeof aspectRatio === 'number' ? `${aspectRatio} / 1` : aspectRatio;
12176
+ }
12177
+ if (width != null) rootStyle.width = width;
12178
+ if (height != null) rootStyle.height = height;
12179
+ return jsxs("div", {
12180
+ "aria-disabled": accessDimmed || undefined,
12181
+ className: cn(v.root(), accessDimmed && 'opacity-60 pointer-events-none', sx, themeSlotProps == null ? void 0 : (_themeSlotProps_root = themeSlotProps.root) == null ? void 0 : _themeSlotProps_root.className, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
12182
+ style: Object.keys(rootStyle).length ? rootStyle : undefined,
12183
+ children: [
12184
+ jsx("img", _extends({
12185
+ ref: handleRef,
12186
+ src: src,
12187
+ alt: alt,
12188
+ loading: loading,
12189
+ onLoad: handleLoad,
12190
+ onError: handleError
12191
+ }, rest, {
12192
+ className: cn(v.img(), themeSlotProps == null ? void 0 : (_themeSlotProps_img = themeSlotProps.img) == null ? void 0 : _themeSlotProps_img.className, slotProps == null ? void 0 : (_slotProps_img = slotProps.img) == null ? void 0 : _slotProps_img.className),
12193
+ // Hide the broken-image glyph once we know it failed; the
12194
+ // fallback overlay takes its place.
12195
+ style: failed ? {
12196
+ visibility: 'hidden'
12197
+ } : undefined
12198
+ })),
12199
+ showSkeletonOverlay && jsx(Skeleton, {
12200
+ variant: "rectangle",
12201
+ animation: "pulse",
12202
+ sx: cn(v.skeleton(), themeSlotProps == null ? void 0 : (_themeSlotProps_skeleton = themeSlotProps.skeleton) == null ? void 0 : _themeSlotProps_skeleton.className, slotProps == null ? void 0 : (_slotProps_skeleton = slotProps.skeleton) == null ? void 0 : _slotProps_skeleton.className)
12203
+ }),
12204
+ failed && jsx("div", {
12205
+ className: cn(v.fallback(), themeSlotProps == null ? void 0 : (_themeSlotProps_fallback = themeSlotProps.fallback) == null ? void 0 : _themeSlotProps_fallback.className, slotProps == null ? void 0 : (_slotProps_fallback = slotProps.fallback) == null ? void 0 : _slotProps_fallback.className),
12206
+ children: fallback != null ? fallback : jsx(BrokenImageIcon, {})
12207
+ })
12208
+ ]
12209
+ });
12210
+ });
12211
+ Image.displayName = 'Image';
12212
+
12213
+ /**
12214
+ * `<Video>` variant recipe — the moving-image twin of `imageVariants`.
12215
+ *
12216
+ * Slots:
12217
+ * - `root` — positioned wrapper. Holds the `<video>` plus the
12218
+ * loading skeleton / error-fallback overlays, and owns
12219
+ * the corner radius + overflow clipping. `relative` so
12220
+ * the overlays can be `absolute inset-0`.
12221
+ * - `video` — the `<video>` element. `object-*` (the `fit` axis)
12222
+ * lives here; `block w-full h-full` so it fills the
12223
+ * reserved box when an aspect ratio / dimensions are
12224
+ * given.
12225
+ * - `skeleton` — the loading placeholder overlay (a `<Skeleton>`
12226
+ * stretched over the reserved box).
12227
+ * - `fallback` — the error overlay shown after `onError`.
12228
+ *
12229
+ * Variant axes:
12230
+ * - `fit` — CSS `object-fit`. `cover` (default) crops to fill,
12231
+ * `contain` letterboxes, etc.
12232
+ * - `rounded` — corner radius token, applied to `root` (which clips
12233
+ * the video via `overflow-hidden`).
12234
+ *
12235
+ * Neutral fallback surface auto-inverts through the dashforge preset's
12236
+ * CSS-variable layer (no explicit `dark:`).
12237
+ */ const videoVariants = tv({
12238
+ slots: {
12239
+ root: [
12240
+ 'relative',
12241
+ 'block',
12242
+ 'overflow-hidden'
12243
+ ],
12244
+ video: [
12245
+ 'block',
12246
+ 'w-full',
12247
+ 'h-full'
12248
+ ],
12249
+ skeleton: [
12250
+ 'absolute',
12251
+ 'inset-0',
12252
+ 'h-full',
12253
+ 'w-full'
12254
+ ],
12255
+ fallback: [
12256
+ 'absolute',
12257
+ 'inset-0',
12258
+ 'flex',
12259
+ 'items-center',
12260
+ 'justify-center',
12261
+ // Neutral surface — auto-inverts via CSS vars, no `dark:`.
12262
+ 'bg-neutral-100',
12263
+ 'text-neutral-400'
12264
+ ]
12265
+ },
12266
+ variants: {
12267
+ fit: {
12268
+ cover: {
12269
+ video: 'object-cover'
12270
+ },
12271
+ contain: {
12272
+ video: 'object-contain'
12273
+ },
12274
+ fill: {
12275
+ video: 'object-fill'
12276
+ },
12277
+ none: {
12278
+ video: 'object-none'
12279
+ },
12280
+ 'scale-down': {
12281
+ video: 'object-scale-down'
12282
+ }
12283
+ },
12284
+ rounded: {
12285
+ none: {
12286
+ root: 'rounded-none'
12287
+ },
12288
+ sm: {
12289
+ root: 'rounded-sm'
12290
+ },
12291
+ md: {
12292
+ root: 'rounded-md'
12293
+ },
12294
+ lg: {
12295
+ root: 'rounded-lg'
12296
+ },
12297
+ full: {
12298
+ root: 'rounded-full'
12299
+ }
12300
+ }
12301
+ },
12302
+ defaultVariants: {
12303
+ fit: 'cover',
12304
+ rounded: 'none'
12305
+ }
12306
+ });
12307
+
12308
+ /**
12309
+ * Broken-media glyph — inline SVG fallback shown after a load error
12310
+ * when no custom `fallback` is provided. Inline (no icon-library dep),
12311
+ * mirroring `<Image>`'s broken-image glyph.
12312
+ *
12313
+ * @internal
12314
+ */ function BrokenMediaIcon() {
12315
+ return jsxs("svg", {
12316
+ width: "24",
12317
+ height: "24",
12318
+ viewBox: "0 0 24 24",
12319
+ fill: "none",
12320
+ stroke: "currentColor",
12321
+ strokeWidth: "1.6",
12322
+ strokeLinecap: "round",
12323
+ strokeLinejoin: "round",
12324
+ "aria-hidden": "true",
12325
+ children: [
12326
+ jsx("path", {
12327
+ d: "M2 2 22 22"
12328
+ }),
12329
+ jsx("path", {
12330
+ d: "M10.66 5H14a2 2 0 0 1 2 2v3.34l1 1L22 8v8"
12331
+ }),
12332
+ jsx("path", {
12333
+ d: "M16 16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2"
12334
+ })
12335
+ ]
12336
+ });
12337
+ }
12338
+ /**
12339
+ * Dashforge TW `<Video>` — the moving-image twin of `<Image>`. A
12340
+ * declarative, robust wrapper over the native `<video>`. Zero eval, no
12341
+ * data fetching: it renders a URL you give it and adds what a bare
12342
+ * `<video>` lacks.
12343
+ *
12344
+ * 1. **No layout shift** — pass `aspectRatio` (or `width` + `height`)
12345
+ * and the box is reserved before the video loads.
12346
+ * 2. **Loading skeleton** — a `<Skeleton>` fills the reserved box
12347
+ * while the first frame loads; skipped when a `poster` is given
12348
+ * and for already-buffered video (no flash).
12349
+ * 3. **Graceful error** — on load failure a muted fallback replaces
12350
+ * the player (`fallback` to customise).
12351
+ *
12352
+ * Controls on by default. Any native `<video>` attribute not named here
12353
+ * (`autoPlay`, `loop`, `muted`, `playsInline`, `crossOrigin`, …) is
12354
+ * forwarded; pass `<source>` children for multi-format delivery. The
12355
+ * ref points at the `<video>`.
12356
+ *
12357
+ * @example
12358
+ * ```tsx
12359
+ * <Video src="/demo.mp4" poster="/demo.jpg" aspectRatio={16 / 9} rounded="lg" />
12360
+ * <Video src="/loop.webm" aspectRatio={1} autoPlay loop muted playsInline controls={false} />
12361
+ * ```
12362
+ */ const Video = /*#__PURE__*/ forwardRef(function Video(props, ref) {
12363
+ var _themeSlotProps_root, _slotProps_root, _themeSlotProps_video, _slotProps_video, _themeSlotProps_skeleton, _slotProps_skeleton, _themeSlotProps_fallback, _slotProps_fallback;
12364
+ const themeDefaults = useComponentDefaults('Video');
12365
+ const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
12366
+ const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
12367
+ const { src, poster, aspectRatio, width, height, fit = 'cover', rounded = 'none', controls = true, preload = 'metadata', showSkeleton = true, fallback, access, visibleWhen, sx, slotProps, onLoadedData, onError, style, children } = merged, rest = _object_without_properties_loose(merged, [
12368
+ "src",
12369
+ "poster",
12370
+ "aspectRatio",
12371
+ "width",
12372
+ "height",
12373
+ "fit",
12374
+ "rounded",
12375
+ "controls",
12376
+ "preload",
12377
+ "showSkeleton",
12378
+ "fallback",
12379
+ "access",
12380
+ "visibleWhen",
12381
+ "sx",
12382
+ "slotProps",
12383
+ "onLoadedData",
12384
+ "onError",
12385
+ "style",
12386
+ "children"
12387
+ ]);
12388
+ // Gating — hooks are always called (rules of hooks), the early return
12389
+ // comes after. `visibleWhen` is a no-op outside a <DashForm>.
12390
+ const bridge = useContext(DashFormContext);
12391
+ const isVisible = useEngineVisibility(bridge == null ? void 0 : bridge.engine, visibleWhen);
12392
+ const accessState = useAccessState(access);
12393
+ const [loaded, setLoaded] = useState(false);
12394
+ const [failed, setFailed] = useState(false);
12395
+ // Reset load / error state when the source changes in place — React's
12396
+ // official "adjust state during render" pattern (no effect, no flash).
12397
+ const [prevSrc, setPrevSrc] = useState(src);
12398
+ if (src !== prevSrc) {
12399
+ setPrevSrc(src);
12400
+ setLoaded(false);
12401
+ setFailed(false);
12402
+ }
12403
+ // Forward the ref AND resolve the already-buffered case: a cached
12404
+ // video may reach HAVE_CURRENT_DATA before React attaches the
12405
+ // handler, so we read `.readyState` at mount and skip the skeleton.
12406
+ const handleRef = useCallback((node)=>{
12407
+ if (typeof ref === 'function') ref(node);
12408
+ else if (ref) ref.current = node;
12409
+ if (node && node.readyState >= 2) setLoaded(true);
12410
+ }, [
12411
+ ref
12412
+ ]);
12413
+ const handleLoadedData = (e)=>{
12414
+ setLoaded(true);
12415
+ onLoadedData == null ? void 0 : onLoadedData(e);
12416
+ };
12417
+ const handleError = (e)=>{
12418
+ setFailed(true);
12419
+ onError == null ? void 0 : onError(e);
12420
+ };
12421
+ // Gated out entirely (hooks above already ran — rules of hooks safe).
12422
+ if (!isVisible || !accessState.visible) return null;
12423
+ // A video has no interactive state, so 'disable' / 'readonly' render it
12424
+ // dimmed and non-interactive rather than truly disabled.
12425
+ const accessDimmed = accessState.disabled || accessState.readonly;
12426
+ const v = videoVariants({
12427
+ fit,
12428
+ rounded
12429
+ });
12430
+ // A skeleton needs a reserved box to paint into, and is redundant when
12431
+ // a poster is present (the poster is the loading visual).
12432
+ const hasReservedBox = aspectRatio != null || width != null && height != null;
12433
+ const showSkeletonOverlay = showSkeleton && !loaded && !failed && hasReservedBox && poster == null;
12434
+ const rootStyle = _extends({}, style);
12435
+ if (aspectRatio != null) {
12436
+ rootStyle.aspectRatio = typeof aspectRatio === 'number' ? `${aspectRatio} / 1` : aspectRatio;
12437
+ }
12438
+ if (width != null) rootStyle.width = width;
12439
+ if (height != null) rootStyle.height = height;
12440
+ return jsxs("div", {
12441
+ "aria-disabled": accessDimmed || undefined,
12442
+ className: cn(v.root(), accessDimmed && 'opacity-60 pointer-events-none', sx, themeSlotProps == null ? void 0 : (_themeSlotProps_root = themeSlotProps.root) == null ? void 0 : _themeSlotProps_root.className, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
12443
+ style: Object.keys(rootStyle).length ? rootStyle : undefined,
12444
+ children: [
12445
+ jsx("video", _extends({
12446
+ ref: handleRef,
12447
+ src: src,
12448
+ poster: poster,
12449
+ controls: controls,
12450
+ preload: preload,
12451
+ onLoadedData: handleLoadedData,
12452
+ onError: handleError
12453
+ }, rest, {
12454
+ className: cn(v.video(), themeSlotProps == null ? void 0 : (_themeSlotProps_video = themeSlotProps.video) == null ? void 0 : _themeSlotProps_video.className, slotProps == null ? void 0 : (_slotProps_video = slotProps.video) == null ? void 0 : _slotProps_video.className),
12455
+ // Hide the failed player; the fallback overlay takes its place.
12456
+ style: failed ? {
12457
+ visibility: 'hidden'
12458
+ } : undefined,
12459
+ children: children
12460
+ })),
12461
+ showSkeletonOverlay && jsx(Skeleton, {
12462
+ variant: "rectangle",
12463
+ animation: "pulse",
12464
+ sx: cn(v.skeleton(), themeSlotProps == null ? void 0 : (_themeSlotProps_skeleton = themeSlotProps.skeleton) == null ? void 0 : _themeSlotProps_skeleton.className, slotProps == null ? void 0 : (_slotProps_skeleton = slotProps.skeleton) == null ? void 0 : _slotProps_skeleton.className)
12465
+ }),
12466
+ failed && jsx("div", {
12467
+ className: cn(v.fallback(), themeSlotProps == null ? void 0 : (_themeSlotProps_fallback = themeSlotProps.fallback) == null ? void 0 : _themeSlotProps_fallback.className, slotProps == null ? void 0 : (_slotProps_fallback = slotProps.fallback) == null ? void 0 : _slotProps_fallback.className),
12468
+ children: fallback != null ? fallback : jsx(BrokenMediaIcon, {})
12469
+ })
12470
+ ]
12471
+ });
12472
+ });
12473
+ Video.displayName = 'Video';
12474
+
11816
12475
  /**
11817
12476
  * `boxVariants` — the surface primitive for @dashforge/tw.
11818
12477
  *
@@ -14878,136 +15537,6 @@ const accordionVariants = tv({
14878
15537
  });
14879
15538
  }
14880
15539
 
14881
- /**
14882
- * `<Skeleton>` variant recipe.
14883
- *
14884
- * Slots:
14885
- * - `root` — single element (atomic component)
14886
- *
14887
- * Variant axes:
14888
- * - `variant` (`text` | `rectangle` | `circle`) — shape / radius
14889
- * - `animation` (`pulse` | `wave` | `none`) — how the placeholder
14890
- * "breathes" while data loads. Always gated on
14891
- * `prefers-reduced-motion` (no exception — animations are
14892
- * purely cosmetic).
14893
- *
14894
- * Implementation notes:
14895
- *
14896
- * - `pulse` uses Tailwind's built-in `animate-pulse` (opacity
14897
- * 1.0 ↔ 0.5 over 2s, ease-in-out infinite).
14898
- * - `wave` overlays a moving gradient. Implemented with a
14899
- * `relative` container + an `::after` pseudo-element via a
14900
- * Tailwind arbitrary-value class. The gradient slides
14901
- * horizontally over the surface, mimicking the MUI Skeleton
14902
- * "wave" look.
14903
- * - Color uses neutral-200 (light) and neutral-800 (dark) for
14904
- * the base, picked up automatically by the dashforge preset's
14905
- * CSS-variable layer.
14906
- *
14907
- * Accessibility: the root has `aria-hidden="true"` (set on the JSX
14908
- * side, not here). Screen readers skip the skeleton entirely;
14909
- * the surrounding component is responsible for announcing
14910
- * loading state via `aria-busy` or `aria-live` if needed.
14911
- */ const skeletonVariants = tv({
14912
- slots: {
14913
- root: [
14914
- 'block',
14915
- // Neutral palette auto-inverts via CSS-var swap (no `dark:`).
14916
- 'bg-neutral-200',
14917
- 'motion-reduce:animate-none'
14918
- ]
14919
- },
14920
- variants: {
14921
- variant: {
14922
- text: {
14923
- root: 'rounded h-[1em] w-full'
14924
- },
14925
- rectangle: {
14926
- root: 'rounded-md w-full h-[100px]'
14927
- },
14928
- circle: {
14929
- root: 'rounded-full w-10 h-10'
14930
- }
14931
- },
14932
- animation: {
14933
- pulse: {
14934
- root: 'animate-pulse'
14935
- },
14936
- wave: {
14937
- root: [
14938
- 'relative overflow-hidden',
14939
- // The wave is rendered via an arbitrary-value ::after
14940
- // pseudo-element with a moving linear gradient. Tailwind
14941
- // doesn't ship a `wave` keyframe; we use the built-in
14942
- // `animate-pulse` as a fallback if a consumer needs to
14943
- // override. The gradient stop direction works in both
14944
- // light and dark mode (white shimmer in light, lighter
14945
- // overlay in dark).
14946
- 'after:absolute after:inset-0',
14947
- 'after:bg-gradient-to-r after:from-transparent after:via-white/40 after:to-transparent',
14948
- 'dark:after:via-white/10',
14949
- 'after:animate-pulse',
14950
- 'after:motion-reduce:animate-none'
14951
- ]
14952
- },
14953
- none: {
14954
- root: ''
14955
- }
14956
- }
14957
- },
14958
- defaultVariants: {
14959
- variant: 'text',
14960
- animation: 'pulse'
14961
- }
14962
- });
14963
-
14964
- /**
14965
- * Dashforge TW `<Skeleton>` — loading placeholder primitive.
14966
- *
14967
- * Renders as a non-interactive `<span aria-hidden="true">` so screen
14968
- * readers skip it. The surrounding component is responsible for
14969
- * announcing loading state via `aria-busy` or `aria-live` if needed.
14970
- *
14971
- * Variants:
14972
- * - `variant` — `text` (default, line shape) / `rectangle` / `circle`
14973
- * - `animation` — `pulse` (default) / `wave` / `none`
14974
- *
14975
- * Both animations are gated by `prefers-reduced-motion` (WCAG 2.3.3).
14976
- *
14977
- * @example
14978
- * ```tsx
14979
- * <Skeleton variant="text" width="200px" />
14980
- * <Skeleton variant="rectangle" width="100%" height="120px" />
14981
- * <Skeleton variant="circle" width="40px" />
14982
- * ```
14983
- */ function Skeleton(props) {
14984
- var _themeSlotProps_root, _slotProps_root;
14985
- const themeDefaults = useComponentDefaults('Skeleton');
14986
- const merged = _extends({}, themeDefaults == null ? void 0 : themeDefaults.defaults, props);
14987
- const themeSlotProps = themeDefaults == null ? void 0 : themeDefaults.slotProps;
14988
- const { variant = 'text', animation = 'pulse', width, height, sx, slotProps } = merged;
14989
- const v = skeletonVariants({
14990
- variant,
14991
- animation
14992
- });
14993
- // Width / height come through as inline style — Tailwind doesn't
14994
- // know about arbitrary user-supplied lengths. We default `circle`'s
14995
- // height to `width` so consumers don't have to repeat themselves.
14996
- const style = {};
14997
- if (width != null) style.width = width;
14998
- if (height != null) {
14999
- style.height = height;
15000
- } else if (variant === 'circle' && width != null) {
15001
- style.height = width;
15002
- }
15003
- return jsx("span", {
15004
- "aria-hidden": "true",
15005
- role: "presentation",
15006
- className: cn(v.root(), sx, themeSlotProps == null ? void 0 : (_themeSlotProps_root = themeSlotProps.root) == null ? void 0 : _themeSlotProps_root.className, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
15007
- style: style
15008
- });
15009
- }
15010
-
15011
15540
  /**
15012
15541
  * `<Pagination>` variant recipe.
15013
15542
  *
@@ -18792,6 +19321,6 @@ function SearchIcon(props) {
18792
19321
  */ // ───── Components ─────
18793
19322
  /**
18794
19323
  * Package version (synced with `package.json` at publish time).
18795
- */ const VERSION = '1.1.1';
19324
+ */ const VERSION = '1.4.0';
18796
19325
 
18797
- export { AVATAR_SOFT_FALLBACK, AVATAR_TONE_FALLBACK, Accordion, Alert, AlertTitle, AppShell, AspectRatio, Autocomplete, Avatar, AvatarGroup, Badge, Box, Breadcrumbs, Button, Calendar, Card, CardActionArea, CardContent, Checkbox, Chip, ConfirmDialogProvider, Container, DRAWER_SIZE_PRESETS, DataGrid, DatePicker, DateRangePicker, DateTimePicker, Dialog, Divider, Drawer, Grid, ICON_BUTTON_BASE, ICON_BUTTON_SIZE_OVERRIDES, IconButton, LeftNav, Link, Menu, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuSkeleton, MenuTrigger, NumberField, OTPField, Pagination, Popover, Progress, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, SPINNER_COLORS, SPINNER_STROKE_WIDTH, SPINNER_TRACK_OPACITY, Select, Skeleton, Slider, SnackbarProvider, Spinner, Stack, Step, Stepper, Switch, Table, Tabs, TextField, Textarea, TimePicker, Tooltip, TopBar, Typography, VERSION, VisuallyHidden, accordionVariants, alertVariants, appShellVariants, autocompleteVariants, avatarVariants, badgeVariants, boxVariants, breadcrumbsVariants, buttonVariants, calendarDayVariants, calendarVariants, checkboxVariants, chipVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, datePickerVariants, dateRangeDayVariants, dateRangePickerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, drawerVariants, getNestedValue, gridVariants, leftNavVariants, linkVariants, menuVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, progressVariants, radioGroupVariants, selectVariants, skeletonVariants, sliderVariants, snackbarVariants, spinnerVariants, stackVariants, stepperVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, timePickerVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar, useStep };
19326
+ export { AVATAR_SOFT_FALLBACK, AVATAR_TONE_FALLBACK, Accordion, Alert, AlertTitle, AppShell, AspectRatio, Autocomplete, Avatar, AvatarGroup, Badge, Box, Breadcrumbs, Button, Calendar, Card, CardActionArea, CardContent, Checkbox, Chip, ConfirmDialogProvider, Container, DRAWER_SIZE_PRESETS, DataGrid, DatePicker, DateRangePicker, DateTimePicker, Dialog, Divider, Drawer, Grid, ICON_BUTTON_BASE, ICON_BUTTON_SIZE_OVERRIDES, IconButton, Image, LeftNav, Link, Menu, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuSkeleton, MenuTrigger, NumberField, OTPField, Pagination, Popover, Progress, RadioGroup, RenderButton, RenderChip, RenderText, RenderTwoLine, RowActionsMenu, SPINNER_COLORS, SPINNER_STROKE_WIDTH, SPINNER_TRACK_OPACITY, Select, Skeleton, Slider, SnackbarProvider, Spinner, Stack, Step, Stepper, Switch, Table, Tabs, TextField, Textarea, TimePicker, Tooltip, TopBar, Typography, VERSION, Video, VisuallyHidden, accordionVariants, alertVariants, appShellVariants, autocompleteVariants, avatarVariants, badgeVariants, boxVariants, breadcrumbsVariants, buttonVariants, calendarDayVariants, calendarVariants, checkboxVariants, chipVariants, cn, confirmDialogVariants, containerVariants, dataGridVariants, datePickerVariants, dateRangeDayVariants, dateRangePickerVariants, dateTimePickerVariants, dialogVariants, dividerLineVariants, dividerVariants, drawerVariants, getNestedValue, gridVariants, imageVariants, leftNavVariants, linkVariants, menuVariants, numberFieldVariants, otpFieldVariants, paginationVariants, popoverVariants, progressVariants, radioGroupVariants, selectVariants, skeletonVariants, sliderVariants, snackbarVariants, spinnerVariants, stackVariants, stepperVariants, switchVariants, tableVariants, tabsVariants, textFieldVariants, textareaVariants, timePickerVariants, tooltipVariants, topBarVariants, typographyVariants, useAccessState, useConfirm, useSnackbar, useStep, videoVariants };