@x-plat/design-system 0.5.3 → 0.5.5

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.cjs CHANGED
@@ -8974,6 +8974,8 @@ var TableCell = import_react34.default.memo((props) => {
8974
8974
  children,
8975
8975
  align = "center",
8976
8976
  isSticky = false,
8977
+ width,
8978
+ nowrap = false,
8977
8979
  onClick
8978
8980
  } = props;
8979
8981
  const { registerStickyCell, stickyCells } = useTableRowContext();
@@ -9017,9 +9019,13 @@ var TableCell = import_react34.default.memo((props) => {
9017
9019
  isSticky && "table-sticky",
9018
9020
  isLastSticky && stickyShadow && "right-shadow",
9019
9021
  onClick && "clickable",
9020
- enableHover && "cell-hover"
9022
+ enableHover && "cell-hover",
9023
+ nowrap && "nowrap"
9021
9024
  ),
9022
- style: isSticky ? { left } : void 0,
9025
+ style: {
9026
+ ...isSticky ? { left } : null,
9027
+ ...width != null ? { width: typeof width === "number" ? `${width}px` : width } : null
9028
+ },
9023
9029
  onClick,
9024
9030
  children
9025
9031
  }
@@ -9275,6 +9281,16 @@ var Tooltip_default = Tooltip;
9275
9281
  // src/components/Video/Video.tsx
9276
9282
  var import_react39 = __toESM(require("react"), 1);
9277
9283
  var import_jsx_runtime346 = require("react/jsx-runtime");
9284
+ var PipIcon = () => /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
9285
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
9286
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("rect", { x: "12", y: "11", width: "7", height: "6", rx: "1", fill: "currentColor" })
9287
+ ] });
9288
+ var formatTime = (sec) => {
9289
+ if (!Number.isFinite(sec) || sec < 0) return "0:00";
9290
+ const m = Math.floor(sec / 60);
9291
+ const s = Math.floor(sec % 60);
9292
+ return `${m}:${s.toString().padStart(2, "0")}`;
9293
+ };
9278
9294
  var Video = import_react39.default.forwardRef((props, ref) => {
9279
9295
  const {
9280
9296
  src,
@@ -9283,13 +9299,36 @@ var Video = import_react39.default.forwardRef((props, ref) => {
9283
9299
  muted,
9284
9300
  loop,
9285
9301
  playsInline,
9302
+ showControls = true,
9303
+ showCenterPlay = true,
9304
+ playbackRates,
9305
+ showCaptions = false,
9306
+ showPip = false,
9307
+ showDownload = false,
9308
+ downloadFileName,
9286
9309
  onPlay,
9287
9310
  onPause,
9311
+ onTimeUpdate,
9312
+ onVolumeChange,
9313
+ onLoadedMetadata,
9314
+ onRateChange,
9315
+ children,
9288
9316
  ...rest
9289
9317
  } = props;
9318
+ const containerRef = import_react39.default.useRef(null);
9290
9319
  const videoRef = import_react39.default.useRef(null);
9291
9320
  const [isPlaying, setIsPlaying] = import_react39.default.useState(Boolean(autoPlay));
9292
9321
  const [isLoaded, setIsLoaded] = import_react39.default.useState(false);
9322
+ const [currentTime, setCurrentTime] = import_react39.default.useState(0);
9323
+ const [duration, setDuration] = import_react39.default.useState(0);
9324
+ const [buffered, setBuffered] = import_react39.default.useState(0);
9325
+ const [volume, setVolume] = import_react39.default.useState(1);
9326
+ const [isMuted, setIsMuted] = import_react39.default.useState(Boolean(muted));
9327
+ const [isFullscreen, setIsFullscreen] = import_react39.default.useState(false);
9328
+ const [playbackRate, setPlaybackRate] = import_react39.default.useState(1);
9329
+ const [rateMenuOpen, setRateMenuOpen] = import_react39.default.useState(false);
9330
+ const [captionsOn, setCaptionsOn] = import_react39.default.useState(false);
9331
+ const [isPip, setIsPip] = import_react39.default.useState(false);
9293
9332
  const setRefs = import_react39.default.useCallback(
9294
9333
  (el) => {
9295
9334
  videoRef.current = el;
@@ -9298,6 +9337,25 @@ var Video = import_react39.default.forwardRef((props, ref) => {
9298
9337
  },
9299
9338
  [ref]
9300
9339
  );
9340
+ import_react39.default.useEffect(() => {
9341
+ const onFsChange = () => {
9342
+ setIsFullscreen(document.fullscreenElement === containerRef.current);
9343
+ };
9344
+ document.addEventListener("fullscreenchange", onFsChange);
9345
+ return () => document.removeEventListener("fullscreenchange", onFsChange);
9346
+ }, []);
9347
+ import_react39.default.useEffect(() => {
9348
+ const v = videoRef.current;
9349
+ if (!v) return;
9350
+ const onEnter = () => setIsPip(true);
9351
+ const onLeave = () => setIsPip(false);
9352
+ v.addEventListener("enterpictureinpicture", onEnter);
9353
+ v.addEventListener("leavepictureinpicture", onLeave);
9354
+ return () => {
9355
+ v.removeEventListener("enterpictureinpicture", onEnter);
9356
+ v.removeEventListener("leavepictureinpicture", onLeave);
9357
+ };
9358
+ }, []);
9301
9359
  const handlePlay = (e) => {
9302
9360
  setIsPlaying(true);
9303
9361
  onPlay?.(e);
@@ -9306,50 +9364,278 @@ var Video = import_react39.default.forwardRef((props, ref) => {
9306
9364
  setIsPlaying(false);
9307
9365
  onPause?.(e);
9308
9366
  };
9309
- const handleLoadedData = () => {
9367
+ const handleLoadedMetadata = (e) => {
9368
+ const v = e.currentTarget;
9369
+ setDuration(v.duration);
9310
9370
  setIsLoaded(true);
9371
+ onLoadedMetadata?.(e);
9372
+ };
9373
+ const handleTimeUpdate = (e) => {
9374
+ const v = e.currentTarget;
9375
+ setCurrentTime(v.currentTime);
9376
+ if (v.buffered.length > 0) {
9377
+ setBuffered(v.buffered.end(v.buffered.length - 1));
9378
+ }
9379
+ onTimeUpdate?.(e);
9380
+ };
9381
+ const handleVolumeChange = (e) => {
9382
+ const v = e.currentTarget;
9383
+ setVolume(v.volume);
9384
+ setIsMuted(v.muted);
9385
+ onVolumeChange?.(e);
9386
+ };
9387
+ const handleRateChange = (e) => {
9388
+ setPlaybackRate(e.currentTarget.playbackRate);
9389
+ onRateChange?.(e);
9311
9390
  };
9312
9391
  const togglePlay = () => {
9313
- if (!videoRef.current) return;
9314
- if (isPlaying) {
9315
- videoRef.current.pause();
9392
+ const v = videoRef.current;
9393
+ if (!v) return;
9394
+ if (v.paused) v.play();
9395
+ else v.pause();
9396
+ };
9397
+ const toggleMute = () => {
9398
+ const v = videoRef.current;
9399
+ if (!v) return;
9400
+ v.muted = !v.muted;
9401
+ };
9402
+ const toggleFullscreen = () => {
9403
+ const el = containerRef.current;
9404
+ if (!el) return;
9405
+ if (document.fullscreenElement === el) {
9406
+ document.exitFullscreen();
9316
9407
  } else {
9317
- videoRef.current.play();
9408
+ el.requestFullscreen?.();
9318
9409
  }
9319
9410
  };
9320
- return /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("div", { className: "lib-xplat-video custom-overlay", children: [
9321
- /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9322
- "video",
9323
- {
9324
- ref: setRefs,
9325
- src,
9326
- poster,
9327
- autoPlay,
9328
- muted,
9329
- loop,
9330
- playsInline: playsInline ?? true,
9331
- onPlay: handlePlay,
9332
- onPause: handlePause,
9333
- onLoadedData: handleLoadedData,
9334
- onClick: togglePlay,
9335
- ...rest
9411
+ const togglePip = async () => {
9412
+ const v = videoRef.current;
9413
+ if (!v) return;
9414
+ try {
9415
+ if (document.pictureInPictureElement === v) {
9416
+ await document.exitPictureInPicture?.();
9417
+ } else {
9418
+ await v.requestPictureInPicture?.();
9336
9419
  }
9337
- ),
9338
- /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9339
- "button",
9340
- {
9341
- type: "button",
9342
- className: clsx_default(
9343
- "play-overlay",
9344
- isPlaying && "is-playing",
9345
- !isLoaded && "is-loading"
9346
- ),
9347
- onClick: togglePlay,
9348
- "aria-label": isPlaying ? "\uC77C\uC2DC\uC815\uC9C0" : "\uC7AC\uC0DD",
9349
- children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(PauseIcon_default, {}) : /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("span", { className: "play-icon", children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(PlayCircleIcon_default, {}) })
9420
+ } catch {
9421
+ }
9422
+ };
9423
+ const toggleCaptions = () => {
9424
+ const v = videoRef.current;
9425
+ if (!v) return;
9426
+ const next = !captionsOn;
9427
+ for (let i = 0; i < v.textTracks.length; i++) {
9428
+ const t = v.textTracks[i];
9429
+ if (t.kind === "captions" || t.kind === "subtitles") {
9430
+ t.mode = next ? "showing" : "hidden";
9350
9431
  }
9351
- )
9352
- ] });
9432
+ }
9433
+ setCaptionsOn(next);
9434
+ };
9435
+ const selectRate = (rate) => {
9436
+ const v = videoRef.current;
9437
+ if (!v) return;
9438
+ v.playbackRate = rate;
9439
+ setRateMenuOpen(false);
9440
+ };
9441
+ const handleSeek = (e) => {
9442
+ const v = videoRef.current;
9443
+ if (!v) return;
9444
+ const next = Number(e.target.value);
9445
+ v.currentTime = next;
9446
+ setCurrentTime(next);
9447
+ };
9448
+ const handleVolumeSlider = (e) => {
9449
+ const v = videoRef.current;
9450
+ if (!v) return;
9451
+ const next = Number(e.target.value);
9452
+ v.volume = next;
9453
+ if (next > 0 && v.muted) v.muted = false;
9454
+ if (next === 0) v.muted = true;
9455
+ };
9456
+ const progressPct = duration > 0 ? currentTime / duration * 100 : 0;
9457
+ const bufferedPct = duration > 0 ? buffered / duration * 100 : 0;
9458
+ const volumePct = (isMuted ? 0 : volume) * 100;
9459
+ const VolumeGlyph = isMuted || volume === 0 ? VolumeXIcon_default : volume < 0.5 ? VolumeIcon_default : Volume2Icon_default;
9460
+ const pipSupported = typeof document !== "undefined" && "pictureInPictureEnabled" in document && document.pictureInPictureEnabled;
9461
+ return /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)(
9462
+ "div",
9463
+ {
9464
+ ref: containerRef,
9465
+ className: clsx_default("lib-xplat-video", showControls && "has-controls"),
9466
+ children: [
9467
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9468
+ "video",
9469
+ {
9470
+ ref: setRefs,
9471
+ src,
9472
+ poster,
9473
+ autoPlay,
9474
+ muted,
9475
+ loop,
9476
+ playsInline: playsInline ?? true,
9477
+ onPlay: handlePlay,
9478
+ onPause: handlePause,
9479
+ onLoadedMetadata: handleLoadedMetadata,
9480
+ onTimeUpdate: handleTimeUpdate,
9481
+ onVolumeChange: handleVolumeChange,
9482
+ onRateChange: handleRateChange,
9483
+ onClick: togglePlay,
9484
+ ...rest,
9485
+ children
9486
+ }
9487
+ ),
9488
+ showCenterPlay && /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9489
+ "button",
9490
+ {
9491
+ type: "button",
9492
+ className: clsx_default(
9493
+ "center-play",
9494
+ isPlaying && "is-playing",
9495
+ !isLoaded && "is-loading"
9496
+ ),
9497
+ onClick: togglePlay,
9498
+ "aria-label": isPlaying ? "\uC77C\uC2DC\uC815\uC9C0" : "\uC7AC\uC0DD",
9499
+ tabIndex: -1,
9500
+ children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("span", { className: "center-play-icon", children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(PlayCircleIcon_default, {}) })
9501
+ }
9502
+ ),
9503
+ showControls && /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("div", { className: "controls", onClick: (e) => e.stopPropagation(), children: [
9504
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9505
+ "input",
9506
+ {
9507
+ type: "range",
9508
+ className: "seekbar",
9509
+ min: 0,
9510
+ max: duration || 0,
9511
+ step: 0.1,
9512
+ value: currentTime,
9513
+ onChange: handleSeek,
9514
+ "aria-label": "\uC7AC\uC0DD \uC704\uCE58",
9515
+ style: {
9516
+ ["--progress"]: `${progressPct}%`,
9517
+ ["--buffered"]: `${bufferedPct}%`
9518
+ }
9519
+ }
9520
+ ),
9521
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("div", { className: "controls-row", children: [
9522
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9523
+ "button",
9524
+ {
9525
+ type: "button",
9526
+ className: "control-btn",
9527
+ onClick: togglePlay,
9528
+ "aria-label": isPlaying ? "\uC77C\uC2DC\uC815\uC9C0" : "\uC7AC\uC0DD",
9529
+ children: isPlaying ? /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(PauseIcon_default, {}) : /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(PlayIcon_default, {})
9530
+ }
9531
+ ),
9532
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("div", { className: "volume-group", children: [
9533
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9534
+ "button",
9535
+ {
9536
+ type: "button",
9537
+ className: "control-btn",
9538
+ onClick: toggleMute,
9539
+ "aria-label": isMuted ? "\uC74C\uC18C\uAC70 \uD574\uC81C" : "\uC74C\uC18C\uAC70",
9540
+ children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(VolumeGlyph, {})
9541
+ }
9542
+ ),
9543
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9544
+ "input",
9545
+ {
9546
+ type: "range",
9547
+ className: "volume-slider",
9548
+ min: 0,
9549
+ max: 1,
9550
+ step: 0.05,
9551
+ value: isMuted ? 0 : volume,
9552
+ onChange: handleVolumeSlider,
9553
+ "aria-label": "\uBCFC\uB968",
9554
+ style: { ["--volume"]: `${volumePct}%` }
9555
+ }
9556
+ )
9557
+ ] }),
9558
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("span", { className: "time", children: [
9559
+ formatTime(currentTime),
9560
+ " / ",
9561
+ formatTime(duration)
9562
+ ] }),
9563
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("div", { className: "controls-spacer" }),
9564
+ playbackRates && playbackRates.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)("div", { className: clsx_default("rate-group", rateMenuOpen && "is-open"), children: [
9565
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)(
9566
+ "button",
9567
+ {
9568
+ type: "button",
9569
+ className: "control-btn rate-btn",
9570
+ onClick: () => setRateMenuOpen((o) => !o),
9571
+ "aria-label": "\uC7AC\uC0DD \uC18D\uB3C4",
9572
+ "aria-expanded": rateMenuOpen,
9573
+ children: [
9574
+ playbackRate,
9575
+ "x"
9576
+ ]
9577
+ }
9578
+ ),
9579
+ rateMenuOpen && /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("ul", { className: "rate-menu", role: "menu", children: playbackRates.map((r2) => /* @__PURE__ */ (0, import_jsx_runtime346.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime346.jsxs)(
9580
+ "button",
9581
+ {
9582
+ type: "button",
9583
+ role: "menuitem",
9584
+ className: clsx_default("rate-item", r2 === playbackRate && "is-active"),
9585
+ onClick: () => selectRate(r2),
9586
+ children: [
9587
+ r2,
9588
+ "x"
9589
+ ]
9590
+ }
9591
+ ) }, r2)) })
9592
+ ] }),
9593
+ showCaptions && /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9594
+ "button",
9595
+ {
9596
+ type: "button",
9597
+ className: clsx_default("control-btn", captionsOn && "is-active"),
9598
+ onClick: toggleCaptions,
9599
+ "aria-label": captionsOn ? "\uC790\uB9C9 \uB044\uAE30" : "\uC790\uB9C9 \uCF1C\uAE30",
9600
+ "aria-pressed": captionsOn,
9601
+ children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(TypeIcon_default, {})
9602
+ }
9603
+ ),
9604
+ showPip && pipSupported && /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9605
+ "button",
9606
+ {
9607
+ type: "button",
9608
+ className: clsx_default("control-btn", isPip && "is-active"),
9609
+ onClick: togglePip,
9610
+ "aria-label": isPip ? "PIP \uC885\uB8CC" : "PIP",
9611
+ children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(PipIcon, {})
9612
+ }
9613
+ ),
9614
+ showDownload && /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9615
+ "a",
9616
+ {
9617
+ className: "control-btn",
9618
+ href: src,
9619
+ download: downloadFileName ?? true,
9620
+ "aria-label": "\uB2E4\uC6B4\uB85C\uB4DC",
9621
+ children: /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(DownloadIcon_default, {})
9622
+ }
9623
+ ),
9624
+ /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(
9625
+ "button",
9626
+ {
9627
+ type: "button",
9628
+ className: "control-btn",
9629
+ onClick: toggleFullscreen,
9630
+ "aria-label": isFullscreen ? "\uC804\uCCB4\uD654\uBA74 \uC885\uB8CC" : "\uC804\uCCB4\uD654\uBA74",
9631
+ children: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(MinimizeIcon_default, {}) : /* @__PURE__ */ (0, import_jsx_runtime346.jsx)(MaximizeIcon_default, {})
9632
+ }
9633
+ )
9634
+ ] })
9635
+ ] })
9636
+ ]
9637
+ }
9638
+ );
9353
9639
  });
9354
9640
  Video.displayName = "Video";
9355
9641
  var Video_default = Video;