sellmate-design-system-react 0.3.0 → 0.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.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  'use strict';
2
3
 
3
4
  var clsx = require('clsx');
@@ -40,7 +41,6 @@ var RDialog__namespace = /*#__PURE__*/_interopNamespace(RDialog);
40
41
  var RCheckbox__namespace = /*#__PURE__*/_interopNamespace(RCheckbox);
41
42
  var Ariakit__namespace = /*#__PURE__*/_interopNamespace(Ariakit);
42
43
 
43
- // src/lib/cn.ts
44
44
  var twMerge = tailwindMerge.extendTailwindMerge({
45
45
  extend: {
46
46
  classGroups: {
@@ -6660,7 +6660,7 @@ var STable = react.forwardRef(function STable2({
6660
6660
  ref: scrollRef,
6661
6661
  onScroll: handleScroll,
6662
6662
  className: cn(
6663
- "relative overflow-auto border border-solid border-[color:var(--cmp-table-border-color)]",
6663
+ "bg-white relative overflow-auto border border-solid border-[color:var(--cmp-table-border-color)]",
6664
6664
  radiusClass,
6665
6665
  // 원본 sd-table__scroll-container--loading/--no-data: 로딩·데이터 없음 시 스크롤·인터랙션 차단
6666
6666
  (isLoading || rowCount === 0) && "overflow-hidden pointer-events-none"
@@ -9834,6 +9834,854 @@ var SBarcodeInput = react.forwardRef(
9834
9834
  }
9835
9835
  );
9836
9836
 
9837
+ // src/components/STimePicker/timepicker.config.ts
9838
+ var V2 = (path) => `var(--cmp-timepicker-${path})`;
9839
+ var TIMEPICKER_SIZES = ["sm", "md"];
9840
+ var TIMEPICKER_SIZE_CONFIG = {
9841
+ sm: {
9842
+ height: V2("sm-height"),
9843
+ paddingX: V2("sm-paddingX"),
9844
+ gap: V2("sm-gap"),
9845
+ icon: V2("sm-icon"),
9846
+ radius: V2("sm-radius"),
9847
+ // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
9848
+ fontSize: 12,
9849
+ lineHeight: 20,
9850
+ minWidth: 80,
9851
+ fieldMinWidth: 128
9852
+ },
9853
+ md: {
9854
+ height: V2("md-height"),
9855
+ paddingX: V2("md-paddingX"),
9856
+ gap: V2("md-gap"),
9857
+ icon: V2("md-icon"),
9858
+ radius: V2("md-radius"),
9859
+ // TODO: component.timepicker 토큰에 typography가 추가되면 CSS 변수로 교체한다.
9860
+ fontSize: 14,
9861
+ lineHeight: 24,
9862
+ minWidth: 96,
9863
+ fieldMinWidth: 160
9864
+ }
9865
+ };
9866
+ function clampTimePart(value, min, max) {
9867
+ if (!Number.isFinite(value)) return min;
9868
+ return Math.min(Math.max(value, min), max);
9869
+ }
9870
+ function padTimePart(value) {
9871
+ return String(value).padStart(2, "0");
9872
+ }
9873
+ function parseTimeValue(value) {
9874
+ const match = /^(\d{1,2}):(\d{1,2})$/.exec(value ?? "");
9875
+ if (!match) return { hour: 0, minute: 0 };
9876
+ return {
9877
+ hour: clampTimePart(Number(match[1]), 0, 23),
9878
+ minute: clampTimePart(Number(match[2]), 0, 59)
9879
+ };
9880
+ }
9881
+ function formatTimeValue(hour, minute) {
9882
+ return `${padTimePart(clampTimePart(hour, 0, 23))}:${padTimePart(clampTimePart(minute, 0, 59))}`;
9883
+ }
9884
+ function formatTimeDisplay(value, type) {
9885
+ if (value == null || value === "") return "";
9886
+ const { hour, minute } = parseTimeValue(value);
9887
+ if (type === "default") return formatTimeValue(hour, minute);
9888
+ const meridiem = hour >= 12 ? "\uC624\uD6C4" : "\uC624\uC804";
9889
+ const displayHour = hour % 12 === 0 ? 12 : hour % 12;
9890
+ return `${meridiem} ${padTimePart(displayHour)}:${padTimePart(minute)}`;
9891
+ }
9892
+ function TimeSelector({
9893
+ value,
9894
+ useMeridiem,
9895
+ minuteStep,
9896
+ onChange
9897
+ }) {
9898
+ const parsed = react.useMemo(() => parseTimeValue(value), [value]);
9899
+ const [hour, setHour] = react.useState(parsed.hour);
9900
+ const [minute, setMinute] = react.useState(parsed.minute);
9901
+ const [editingPart, setEditingPart] = react.useState(null);
9902
+ const [inputDraft, setInputDraft] = react.useState({});
9903
+ react.useEffect(() => {
9904
+ setHour(parsed.hour);
9905
+ setMinute(parsed.minute);
9906
+ }, [parsed.hour, parsed.minute]);
9907
+ const meridiem = hour >= 12 ? "PM" : "AM";
9908
+ const displayHour = useMeridiem ? hour % 12 === 0 ? 12 : hour % 12 : hour;
9909
+ const commit = (nextHour, nextMinute) => {
9910
+ const h = clampTimePart(nextHour, 0, 23);
9911
+ const m = clampTimePart(nextMinute, 0, 59);
9912
+ setHour(h);
9913
+ setMinute(m);
9914
+ onChange(formatTimeValue(h, m));
9915
+ };
9916
+ const setMeridiem = (next) => {
9917
+ if (next === meridiem) return;
9918
+ commit(next === "AM" ? hour - 12 : hour + 12, minute);
9919
+ };
9920
+ const changeMinute = (delta) => {
9921
+ const next = minute + delta;
9922
+ if (next > 59) {
9923
+ commit((hour + 1) % 24, next - 60);
9924
+ return;
9925
+ }
9926
+ if (next < 0) {
9927
+ commit((hour + 23) % 24, next + 60);
9928
+ return;
9929
+ }
9930
+ commit(hour, next);
9931
+ };
9932
+ const changeHour = (delta) => commit((hour + delta + 24) % 24, minute);
9933
+ const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
9934
+ const beginEdit = (part, currentValue) => {
9935
+ setEditingPart(part);
9936
+ setInputDraft((prev) => ({ ...prev, [part]: String(currentValue).padStart(2, "0") }));
9937
+ };
9938
+ const commitHourDraft = (draft) => {
9939
+ const clamped = clampTimePart(Number(draft), useMeridiem ? 1 : 0, useMeridiem ? 12 : 23);
9940
+ const nextHour = useMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
9941
+ commit(nextHour, minute);
9942
+ return clamped;
9943
+ };
9944
+ const commitMinuteDraft = (draft) => {
9945
+ const clamped = clampTimePart(Number(draft), 0, 59);
9946
+ commit(hour, clamped);
9947
+ return clamped;
9948
+ };
9949
+ const endEdit = (part) => {
9950
+ const draft = inputDraft[part];
9951
+ if (part === "hour" && draft != null && draft !== "") {
9952
+ commitHourDraft(draft);
9953
+ }
9954
+ if (part === "minute" && draft != null && draft !== "") {
9955
+ commitMinuteDraft(draft);
9956
+ }
9957
+ setEditingPart(null);
9958
+ setInputDraft({});
9959
+ };
9960
+ const inputStyle = {
9961
+ width: "var(--cmp-timepicker-selector-input-width)",
9962
+ height: "var(--cmp-textinput-sm-height)",
9963
+ borderRadius: "var(--cmp-textinput-sm-radius)",
9964
+ border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
9965
+ color: "var(--cmp-textinput-text-default)",
9966
+ fontSize: 12,
9967
+ lineHeight: "20px"
9968
+ };
9969
+ const renderColumn = (type) => {
9970
+ const isHour = type === "hour";
9971
+ const val = isHour ? displayHour : minute;
9972
+ const displayValue = editingPart === type ? inputDraft[type] ?? "" : String(val).padStart(2, "0");
9973
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9974
+ "div",
9975
+ {
9976
+ className: "flex flex-col items-center justify-center",
9977
+ style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
9978
+ children: [
9979
+ /* @__PURE__ */ jsxRuntime.jsx(
9980
+ SGhostButton,
9981
+ {
9982
+ icon: "chevronUp",
9983
+ size: "xs",
9984
+ ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
9985
+ onClick: () => isHour ? changeHour(1) : changeMinute(minuteStep)
9986
+ }
9987
+ ),
9988
+ /* @__PURE__ */ jsxRuntime.jsx(
9989
+ "input",
9990
+ {
9991
+ "aria-label": isHour ? "\uC2DC" : "\uBD84",
9992
+ inputMode: "numeric",
9993
+ value: displayValue,
9994
+ placeholder: "-",
9995
+ onFocus: () => beginEdit(type, val),
9996
+ onBlur: () => endEdit(type),
9997
+ onChange: (event) => {
9998
+ const draft = sanitizeInput(event.target.value);
9999
+ setInputDraft((prev) => ({ ...prev, [type]: draft }));
10000
+ if (draft === "") return;
10001
+ const raw = Number(draft);
10002
+ if (isHour) {
10003
+ if (draft.length < 2) return;
10004
+ const clamped2 = commitHourDraft(draft);
10005
+ const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
10006
+ setInputDraft((prev) => ({ ...prev, [type]: displayDraft2 }));
10007
+ return;
10008
+ }
10009
+ if (draft.length < 2) return;
10010
+ const clamped = commitMinuteDraft(draft);
10011
+ const displayDraft = raw === clamped ? draft : String(clamped);
10012
+ setInputDraft((prev) => ({ ...prev, [type]: displayDraft }));
10013
+ },
10014
+ className: "box-border bg-white text-center outline-none",
10015
+ style: inputStyle
10016
+ }
10017
+ ),
10018
+ /* @__PURE__ */ jsxRuntime.jsx(
10019
+ SGhostButton,
10020
+ {
10021
+ icon: "chevronDown",
10022
+ size: "xs",
10023
+ ariaLabel: `${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
10024
+ onClick: () => isHour ? changeHour(-1) : changeMinute(-minuteStep)
10025
+ }
10026
+ )
10027
+ ]
10028
+ }
10029
+ );
10030
+ };
10031
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10032
+ "div",
10033
+ {
10034
+ className: "flex flex-col items-stretch justify-center overflow-hidden",
10035
+ style: {
10036
+ background: "var(--cmp-timepicker-selector-bg)",
10037
+ borderRadius: "var(--cmp-timepicker-selector-radius)",
10038
+ boxShadow: "4px 8px 16px -4px rgba(34,34,34,0.1), 0 0 24px -6px rgba(34,34,34,0.12)"
10039
+ },
10040
+ children: [
10041
+ useMeridiem && /* @__PURE__ */ jsxRuntime.jsx(
10042
+ "div",
10043
+ {
10044
+ className: "flex items-center justify-center",
10045
+ style: {
10046
+ padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
10047
+ },
10048
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item, index) => {
10049
+ const selected = item === meridiem;
10050
+ return /* @__PURE__ */ jsxRuntime.jsx(
10051
+ "button",
10052
+ {
10053
+ type: "button",
10054
+ onClick: () => setMeridiem(item),
10055
+ className: cn(
10056
+ "box-border h-[var(--cmp-textinput-sm-height)] w-[61px] cursor-pointer border border-solid bg-white text-center text-[12px] font-medium leading-[20px]",
10057
+ index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)]" : "rounded-r-[var(--cmp-select-sm-radius)]",
10058
+ index === 0 ? "-mr-px" : "",
10059
+ selected ? "z-[1] border-[var(--cmp-timepicker-border-focus)] text-[var(--cmp-timepicker-border-focus)]" : "border-[var(--cmp-timepicker-border-default)] text-[var(--cmp-field-hint-color)]"
10060
+ ),
10061
+ children: item === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
10062
+ },
10063
+ item
10064
+ );
10065
+ }) })
10066
+ }
10067
+ ),
10068
+ useMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px w-full bg-[var(--sys-color-divider-default)]" }),
10069
+ /* @__PURE__ */ jsxRuntime.jsxs(
10070
+ "div",
10071
+ {
10072
+ className: "flex items-center justify-center",
10073
+ style: {
10074
+ gap: "var(--cmp-timepicker-selector-time-gap)",
10075
+ padding: "var(--cmp-timepicker-selector-section-paddingY) var(--cmp-timepicker-selector-section-paddingX)"
10076
+ },
10077
+ children: [
10078
+ renderColumn("hour"),
10079
+ /* @__PURE__ */ jsxRuntime.jsx(
10080
+ "span",
10081
+ {
10082
+ className: "text-center",
10083
+ style: {
10084
+ width: "var(--cmp-timepicker-selector-colon-size)",
10085
+ color: "var(--cmp-field-hint-color)",
10086
+ fontSize: "var(--cmp-timepicker-selector-colon-size)",
10087
+ lineHeight: "var(--cmp-timepicker-selector-colon-size)"
10088
+ },
10089
+ children: ":"
10090
+ }
10091
+ ),
10092
+ renderColumn("minute")
10093
+ ]
10094
+ }
10095
+ )
10096
+ ]
10097
+ }
10098
+ );
10099
+ }
10100
+ function STimePicker({
10101
+ value,
10102
+ onValueChange,
10103
+ onOpenChange,
10104
+ type = "default",
10105
+ size = "sm",
10106
+ placeholder = "00:00",
10107
+ disabled = false,
10108
+ clearable = false,
10109
+ useMeridiem,
10110
+ minuteStep = 1,
10111
+ width,
10112
+ name,
10113
+ rules,
10114
+ status,
10115
+ label,
10116
+ labelWidth,
10117
+ icon,
10118
+ iconColor,
10119
+ labelTooltip,
10120
+ labelTooltipProps,
10121
+ addonLabel,
10122
+ addonAlign,
10123
+ hint,
10124
+ error,
10125
+ errorMessage,
10126
+ className,
10127
+ style
10128
+ }) {
10129
+ const portalContainer = usePortalContainer();
10130
+ const [open, setOpen] = react.useState(false);
10131
+ const [focused, setFocused] = react.useState(false);
10132
+ const [ruleError, setRuleError] = react.useState("");
10133
+ const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
10134
+ const normalized = value ? formatTimeValue(parseTimeValue(value).hour, parseTimeValue(value).minute) : null;
10135
+ const effectiveUseMeridiem = useMeridiem ?? type === "midday";
10136
+ const displayText = formatTimeDisplay(normalized, effectiveUseMeridiem ? "midday" : "default");
10137
+ const hasValue = normalized != null && normalized !== "";
10138
+ useFormField({
10139
+ name,
10140
+ validate: () => {
10141
+ const msg = runRules(value, rules);
10142
+ setRuleError(msg);
10143
+ return msg === "";
10144
+ },
10145
+ resetValidation: () => setRuleError("")
10146
+ });
10147
+ const resolvedError = error || ruleError !== "";
10148
+ const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
10149
+ const commitOpen = (next) => {
10150
+ if (disabled) return;
10151
+ setOpen(next);
10152
+ onOpenChange?.(next);
10153
+ if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
10154
+ };
10155
+ const commitValue = (next) => {
10156
+ onValueChange?.(next);
10157
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
10158
+ };
10159
+ return /* @__PURE__ */ jsxRuntime.jsx(
10160
+ SField,
10161
+ {
10162
+ label,
10163
+ labelWidth,
10164
+ icon,
10165
+ iconColor,
10166
+ labelTooltip,
10167
+ labelTooltipProps,
10168
+ addonLabel,
10169
+ addonAlign,
10170
+ size,
10171
+ hint,
10172
+ error: resolvedError,
10173
+ status: resolvedStatus,
10174
+ focused: focused || open,
10175
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
10176
+ width,
10177
+ disabled,
10178
+ className,
10179
+ style: { minWidth: dims.fieldMinWidth, ...style },
10180
+ children: /* @__PURE__ */ jsxRuntime.jsxs(RPopover2__namespace.Root, { open, onOpenChange: commitOpen, children: [
10181
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
10182
+ "button",
10183
+ {
10184
+ type: "button",
10185
+ name,
10186
+ disabled,
10187
+ onFocus: () => setFocused(true),
10188
+ onBlur: () => setFocused(false),
10189
+ className: "flex h-full w-full items-center overflow-hidden bg-transparent outline-none disabled:cursor-not-allowed",
10190
+ style: { padding: `0 ${dims.paddingX}`, gap: dims.gap, borderRadius: dims.radius },
10191
+ children: [
10192
+ /* @__PURE__ */ jsxRuntime.jsx(
10193
+ SIcon,
10194
+ {
10195
+ name: "clockOutline",
10196
+ size: dims.icon,
10197
+ color: disabled ? "var(--cmp-timepicker-icon-disabled)" : "var(--cmp-timepicker-icon-default)",
10198
+ className: "flex-shrink-0"
10199
+ }
10200
+ ),
10201
+ /* @__PURE__ */ jsxRuntime.jsx(
10202
+ "span",
10203
+ {
10204
+ className: "flex-1 truncate text-center",
10205
+ style: {
10206
+ minWidth: dims.minWidth,
10207
+ fontSize: dims.fontSize,
10208
+ lineHeight: `${dims.lineHeight}px`,
10209
+ color: disabled ? "var(--cmp-timepicker-text-disabled)" : hasValue ? "var(--cmp-timepicker-text-default)" : "var(--cmp-field-hint-color)"
10210
+ },
10211
+ children: hasValue ? displayText : placeholder
10212
+ }
10213
+ ),
10214
+ clearable && hasValue && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
10215
+ SGhostButton,
10216
+ {
10217
+ icon: "close",
10218
+ size: "xs",
10219
+ ariaLabel: "\uC2DC\uAC04 \uC9C0\uC6B0\uAE30",
10220
+ onClick: (event) => {
10221
+ event.stopPropagation();
10222
+ commitValue(null);
10223
+ }
10224
+ }
10225
+ )
10226
+ ]
10227
+ }
10228
+ ) }),
10229
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxRuntime.jsx(
10230
+ RPopover2__namespace.Content,
10231
+ {
10232
+ side: "bottom",
10233
+ align: "start",
10234
+ sideOffset: 4,
10235
+ className: cn("z-50", FLOATING_SLIDE_ANIM),
10236
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10237
+ TimeSelector,
10238
+ {
10239
+ value: normalized,
10240
+ useMeridiem: effectiveUseMeridiem,
10241
+ minuteStep,
10242
+ onChange: commitValue
10243
+ }
10244
+ )
10245
+ }
10246
+ ) })
10247
+ ] })
10248
+ }
10249
+ );
10250
+ }
10251
+
10252
+ // src/components/STimeRangePicker/time-range-picker.config.ts
10253
+ function getTimeMinutes(value) {
10254
+ const { hour, minute } = parseTimeValue(value);
10255
+ return hour * 60 + minute;
10256
+ }
10257
+ function normalizeTimeRangeValue(value, rangeOrder = "strict") {
10258
+ if (value == null) return null;
10259
+ const normalized = value.map((item) => {
10260
+ const parsed = parseTimeValue(item);
10261
+ return formatTimeValue(parsed.hour, parsed.minute);
10262
+ });
10263
+ if (rangeOrder === "strict" && getTimeMinutes(normalized[0]) > getTimeMinutes(normalized[1])) {
10264
+ return [normalized[0], normalized[0]];
10265
+ }
10266
+ return normalized;
10267
+ }
10268
+ function formatTimeRangeDisplay(value, type) {
10269
+ if (value == null || value[0] === "" || value[1] === "") return "";
10270
+ return `${formatTimeDisplay(value[0], type)} ~ ${formatTimeDisplay(value[1], type)}`;
10271
+ }
10272
+ function RangeTimeSelector({
10273
+ value,
10274
+ useMeridiem,
10275
+ minuteStep,
10276
+ rangeOrder,
10277
+ onChange
10278
+ }) {
10279
+ const range = react.useMemo(() => value ?? ["00:00", "23:59"], [value]);
10280
+ const [editingKey, setEditingKey] = react.useState(null);
10281
+ const [inputDraft, setInputDraft] = react.useState({});
10282
+ const autoAdjustedRef = react.useRef(null);
10283
+ const getParts = (side) => parseTimeValue(side === "start" ? range[0] : range[1]);
10284
+ const getMeridiem = (side) => getParts(side).hour >= 12 ? "PM" : "AM";
10285
+ const getDisplayHour = (side) => {
10286
+ const { hour } = getParts(side);
10287
+ if (!useMeridiem) return hour;
10288
+ const displayHour = hour % 12;
10289
+ return displayHour === 0 ? 12 : displayHour;
10290
+ };
10291
+ const commit = (side, hour, minute, direction = "unknown") => {
10292
+ const next = formatTimeValue(hour, minute);
10293
+ onChange(normalizeNextRange(side === "start" ? [next, range[1]] : [range[0], next], side, direction));
10294
+ };
10295
+ const getTimeMinutes2 = (time) => {
10296
+ const { hour, minute } = parseTimeValue(time);
10297
+ return hour * 60 + minute;
10298
+ };
10299
+ const normalizeNextRange = (nextRange, changedSide, direction) => {
10300
+ if (rangeOrder === "allow-cross-day") return nextRange;
10301
+ const [start, end] = nextRange;
10302
+ const [currentStart, currentEnd] = range;
10303
+ if (getTimeMinutes2(start) <= getTimeMinutes2(end)) {
10304
+ const shouldKeepSynced = autoAdjustedRef.current?.side === changedSide && currentStart === currentEnd && start !== end && (direction === "unknown" || autoAdjustedRef.current.direction === direction);
10305
+ if (shouldKeepSynced) {
10306
+ return changedSide === "start" ? [start, start] : [end, end];
10307
+ }
10308
+ autoAdjustedRef.current = null;
10309
+ return nextRange;
10310
+ }
10311
+ autoAdjustedRef.current = {
10312
+ side: changedSide,
10313
+ direction: changedSide === "start" ? "increase" : "decrease"
10314
+ };
10315
+ return changedSide === "start" ? [start, start] : [end, end];
10316
+ };
10317
+ const changeHour = (side, delta) => {
10318
+ const { hour, minute } = getParts(side);
10319
+ commit(side, (hour + delta + 24) % 24, minute, delta > 0 ? "increase" : "decrease");
10320
+ };
10321
+ const changeMinute = (side, delta) => {
10322
+ const { hour, minute } = getParts(side);
10323
+ const next = minute + delta;
10324
+ if (next > 59) {
10325
+ commit(side, (hour + 1) % 24, next - 60, delta > 0 ? "increase" : "decrease");
10326
+ return;
10327
+ }
10328
+ if (next < 0) {
10329
+ commit(side, (hour + 23) % 24, next + 60, delta > 0 ? "increase" : "decrease");
10330
+ return;
10331
+ }
10332
+ commit(side, hour, next, delta > 0 ? "increase" : "decrease");
10333
+ };
10334
+ const setMeridiem = (side, next) => {
10335
+ const current = getMeridiem(side);
10336
+ if (current === next) return;
10337
+ const { hour, minute } = getParts(side);
10338
+ commit(side, next === "AM" ? hour - 12 : hour + 12, minute);
10339
+ };
10340
+ const getEditingKey = (side, part) => `${side}-${part}`;
10341
+ const sanitizeInput = (next) => next.replace(/\D/g, "").slice(0, 2);
10342
+ const beginEdit = (side, part, currentValue) => {
10343
+ const key = getEditingKey(side, part);
10344
+ setEditingKey(key);
10345
+ setInputDraft((prev) => ({ ...prev, [key]: String(currentValue).padStart(2, "0") }));
10346
+ };
10347
+ const commitHourDraft = (side, draft) => {
10348
+ const clamped = clampTimePart(Number(draft), useMeridiem ? 1 : 0, useMeridiem ? 12 : 23);
10349
+ const parts = getParts(side);
10350
+ const meridiem = getMeridiem(side);
10351
+ const nextHour = useMeridiem ? meridiem === "PM" ? clamped === 12 ? 12 : clamped + 12 : clamped === 12 ? 0 : clamped : clamped;
10352
+ commit(side, nextHour, parts.minute);
10353
+ return clamped;
10354
+ };
10355
+ const commitMinuteDraft = (side, draft) => {
10356
+ const { hour } = getParts(side);
10357
+ const clamped = clampTimePart(Number(draft), 0, 59);
10358
+ commit(side, hour, clamped);
10359
+ return clamped;
10360
+ };
10361
+ const endEdit = (side, part) => {
10362
+ const key = getEditingKey(side, part);
10363
+ const draft = inputDraft[key];
10364
+ if (part === "hour" && draft != null && draft !== "") {
10365
+ commitHourDraft(side, draft);
10366
+ }
10367
+ if (part === "minute" && draft != null && draft !== "") {
10368
+ commitMinuteDraft(side, draft);
10369
+ }
10370
+ setEditingKey(null);
10371
+ setInputDraft({});
10372
+ };
10373
+ const inputStyle = {
10374
+ width: "var(--cmp-timepicker-selector-input-width)",
10375
+ height: "var(--cmp-textinput-sm-height)",
10376
+ borderRadius: "var(--cmp-textinput-sm-radius)",
10377
+ border: "var(--cmp-textinput-borderWidth) solid var(--cmp-textinput-border-default)",
10378
+ color: "var(--cmp-textinput-text-default)",
10379
+ fontSize: 12,
10380
+ lineHeight: "20px"
10381
+ };
10382
+ const renderColumn = (side, kind) => {
10383
+ const isHour = kind === "hour";
10384
+ const { minute } = getParts(side);
10385
+ const val = isHour ? getDisplayHour(side) : minute;
10386
+ const currentEditingKey = getEditingKey(side, kind);
10387
+ const displayValue = editingKey === currentEditingKey ? inputDraft[currentEditingKey] ?? "" : String(val).padStart(2, "0");
10388
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10389
+ "div",
10390
+ {
10391
+ className: "flex flex-col items-center justify-center",
10392
+ style: { gap: "var(--cmp-timepicker-selector-time-column-gap)" },
10393
+ children: [
10394
+ /* @__PURE__ */ jsxRuntime.jsx(
10395
+ SGhostButton,
10396
+ {
10397
+ icon: "chevronUp",
10398
+ size: "xs",
10399
+ ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uC99D\uAC00`,
10400
+ onClick: () => isHour ? changeHour(side, 1) : changeMinute(side, minuteStep)
10401
+ }
10402
+ ),
10403
+ /* @__PURE__ */ jsxRuntime.jsx(
10404
+ "input",
10405
+ {
10406
+ "aria-label": `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"}`,
10407
+ inputMode: "numeric",
10408
+ value: displayValue,
10409
+ placeholder: "-",
10410
+ onFocus: () => beginEdit(side, kind, val),
10411
+ onBlur: () => endEdit(side, kind),
10412
+ onChange: (event) => {
10413
+ const draft = sanitizeInput(event.target.value);
10414
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: draft }));
10415
+ if (draft === "") return;
10416
+ const raw = Number(draft);
10417
+ if (isHour) {
10418
+ if (draft.length < 2) return;
10419
+ const clamped2 = commitHourDraft(side, draft);
10420
+ const displayDraft2 = raw === clamped2 ? draft : String(clamped2);
10421
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft2 }));
10422
+ return;
10423
+ }
10424
+ if (draft.length < 2) return;
10425
+ const clamped = commitMinuteDraft(side, draft);
10426
+ const displayDraft = raw === clamped ? draft : String(clamped);
10427
+ setInputDraft((prev) => ({ ...prev, [currentEditingKey]: displayDraft }));
10428
+ },
10429
+ className: "box-border bg-white text-center outline-none",
10430
+ style: inputStyle
10431
+ }
10432
+ ),
10433
+ /* @__PURE__ */ jsxRuntime.jsx(
10434
+ SGhostButton,
10435
+ {
10436
+ icon: "chevronDown",
10437
+ size: "xs",
10438
+ ariaLabel: `${side === "start" ? "\uC2DC\uC791" : "\uC885\uB8CC"} ${isHour ? "\uC2DC" : "\uBD84"} \uAC10\uC18C`,
10439
+ onClick: () => isHour ? changeHour(side, -1) : changeMinute(side, -minuteStep)
10440
+ }
10441
+ )
10442
+ ]
10443
+ }
10444
+ );
10445
+ };
10446
+ const renderPanel = (side, title) => /* @__PURE__ */ jsxRuntime.jsxs(
10447
+ "div",
10448
+ {
10449
+ className: "flex flex-col items-center justify-center",
10450
+ style: { gap: "var(--cmp-timepicker-selector-range-section-gap)" },
10451
+ children: [
10452
+ /* @__PURE__ */ jsxRuntime.jsxs(
10453
+ "div",
10454
+ {
10455
+ className: "flex flex-col items-center",
10456
+ style: { gap: "var(--cmp-timepicker-selector-range-heading-gap)" },
10457
+ children: [
10458
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-nowrap text-center text-[12px] font-bold leading-[20px] text-[var(--cmp-timepicker-selector-range-heading-color)]", children: title }),
10459
+ useMeridiem && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "isolate flex", children: ["AM", "PM"].map((item, index) => {
10460
+ const selected = item === getMeridiem(side);
10461
+ return /* @__PURE__ */ jsxRuntime.jsx(
10462
+ "button",
10463
+ {
10464
+ type: "button",
10465
+ onClick: () => setMeridiem(side, item),
10466
+ className: cn(
10467
+ "box-border h-[var(--cmp-textinput-sm-height)] w-[61px] cursor-pointer border border-solid bg-white text-center text-[12px] font-medium leading-[20px]",
10468
+ index === 0 ? "rounded-l-[var(--cmp-select-sm-radius)] -mr-px" : "rounded-r-[var(--cmp-select-sm-radius)]",
10469
+ selected ? "z-[1] border-[var(--cmp-timepicker-border-focus)] text-[var(--cmp-timepicker-border-focus)]" : "border-[var(--cmp-timepicker-border-default)] text-[var(--cmp-field-hint-color)]"
10470
+ ),
10471
+ children: item === "AM" ? "\uC624\uC804" : "\uC624\uD6C4"
10472
+ },
10473
+ item
10474
+ );
10475
+ }) })
10476
+ ]
10477
+ }
10478
+ ),
10479
+ /* @__PURE__ */ jsxRuntime.jsxs(
10480
+ "div",
10481
+ {
10482
+ className: "flex items-center justify-center",
10483
+ style: {
10484
+ gap: "var(--cmp-timepicker-selector-time-gap)",
10485
+ padding: "0 var(--cmp-timepicker-selector-section-paddingX)"
10486
+ },
10487
+ children: [
10488
+ renderColumn(side, "hour"),
10489
+ /* @__PURE__ */ jsxRuntime.jsx(
10490
+ "span",
10491
+ {
10492
+ className: "text-center",
10493
+ style: {
10494
+ width: "var(--cmp-timepicker-selector-colon-size)",
10495
+ color: "var(--cmp-field-hint-color)",
10496
+ fontSize: "var(--cmp-timepicker-selector-colon-size)",
10497
+ lineHeight: "var(--cmp-timepicker-selector-colon-size)"
10498
+ },
10499
+ children: ":"
10500
+ }
10501
+ ),
10502
+ renderColumn(side, "minute")
10503
+ ]
10504
+ }
10505
+ )
10506
+ ]
10507
+ }
10508
+ );
10509
+ return /* @__PURE__ */ jsxRuntime.jsxs(
10510
+ "div",
10511
+ {
10512
+ className: "flex items-start justify-center overflow-hidden",
10513
+ style: {
10514
+ gap: "var(--cmp-timepicker-selector-range-panelGap)",
10515
+ padding: "var(--cmp-timepicker-selector-range-paddingXY)",
10516
+ background: "var(--cmp-timepicker-selector-bg)",
10517
+ borderRadius: "var(--cmp-timepicker-selector-radius)",
10518
+ boxShadow: "var(--shadow-spread-sm), var(--shadow-spread-md)"
10519
+ },
10520
+ children: [
10521
+ renderPanel("start", "\uC2DC\uC791 \uC2DC\uAC04"),
10522
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "self-stretch w-px bg-[var(--sys-color-divider-default)]" }),
10523
+ renderPanel("end", "\uC885\uB8CC \uC2DC\uAC04")
10524
+ ]
10525
+ }
10526
+ );
10527
+ }
10528
+ function STimeRangePicker({
10529
+ value,
10530
+ onValueChange,
10531
+ onOpenChange,
10532
+ type = "default",
10533
+ size = "sm",
10534
+ placeholder = "00:00 ~ 23:59",
10535
+ disabled = false,
10536
+ clearable = false,
10537
+ useMeridiem,
10538
+ minuteStep = 1,
10539
+ rangeOrder = "strict",
10540
+ width,
10541
+ name,
10542
+ rules,
10543
+ status,
10544
+ label,
10545
+ labelWidth,
10546
+ icon,
10547
+ iconColor,
10548
+ labelTooltip,
10549
+ labelTooltipProps,
10550
+ addonLabel,
10551
+ addonAlign,
10552
+ hint,
10553
+ error,
10554
+ errorMessage,
10555
+ className,
10556
+ style
10557
+ }) {
10558
+ const portalContainer = usePortalContainer();
10559
+ const [open, setOpen] = react.useState(false);
10560
+ const [focused, setFocused] = react.useState(false);
10561
+ const [ruleError, setRuleError] = react.useState("");
10562
+ const dims = TIMEPICKER_SIZE_CONFIG[size] ?? TIMEPICKER_SIZE_CONFIG.sm;
10563
+ const normalized = normalizeTimeRangeValue(value ?? null, rangeOrder);
10564
+ const effectiveUseMeridiem = useMeridiem ?? type === "midday";
10565
+ const displayText = formatTimeRangeDisplay(
10566
+ normalized,
10567
+ effectiveUseMeridiem ? "midday" : "default"
10568
+ );
10569
+ const hasValue = normalized != null && normalized[0] !== "" && normalized[1] !== "";
10570
+ useFormField({
10571
+ name,
10572
+ validate: () => {
10573
+ const msg = runRules(value, rules);
10574
+ setRuleError(msg);
10575
+ return msg === "";
10576
+ },
10577
+ resetValidation: () => setRuleError("")
10578
+ });
10579
+ const resolvedError = error || ruleError !== "";
10580
+ const resolvedStatus = status ?? (rules && rules.length > 0 ? ruleError !== "" ? "error" : void 0 : void 0);
10581
+ const commitOpen = (next) => {
10582
+ if (disabled) return;
10583
+ setOpen(next);
10584
+ onOpenChange?.(next);
10585
+ if (!next && rules && rules.length > 0) setRuleError(runRules(value, rules));
10586
+ };
10587
+ const commitValue = (next) => {
10588
+ onValueChange?.(next);
10589
+ if (rules && rules.length > 0) setRuleError(runRules(next, rules));
10590
+ };
10591
+ return /* @__PURE__ */ jsxRuntime.jsx(
10592
+ SField,
10593
+ {
10594
+ label,
10595
+ labelWidth,
10596
+ icon,
10597
+ iconColor,
10598
+ labelTooltip,
10599
+ labelTooltipProps,
10600
+ addonLabel,
10601
+ addonAlign,
10602
+ size,
10603
+ hint,
10604
+ error: resolvedError,
10605
+ status: resolvedStatus,
10606
+ focused: focused || open,
10607
+ errorMessage: ruleError !== "" ? ruleError : errorMessage,
10608
+ width,
10609
+ disabled,
10610
+ className,
10611
+ style: { minWidth: dims.fieldMinWidth, ...style },
10612
+ children: /* @__PURE__ */ jsxRuntime.jsxs(RPopover2__namespace.Root, { open, onOpenChange: commitOpen, children: [
10613
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
10614
+ "button",
10615
+ {
10616
+ type: "button",
10617
+ name,
10618
+ disabled,
10619
+ onFocus: () => setFocused(true),
10620
+ onBlur: () => setFocused(false),
10621
+ className: "flex h-full w-full items-center overflow-hidden bg-transparent outline-none disabled:cursor-not-allowed",
10622
+ style: { padding: `0 ${dims.paddingX}`, gap: dims.gap, borderRadius: dims.radius },
10623
+ children: [
10624
+ /* @__PURE__ */ jsxRuntime.jsx(
10625
+ SIcon,
10626
+ {
10627
+ name: "clockOutline",
10628
+ size: dims.icon,
10629
+ color: disabled ? "var(--cmp-timepicker-icon-disabled)" : "var(--cmp-timepicker-icon-default)",
10630
+ className: "flex-shrink-0"
10631
+ }
10632
+ ),
10633
+ /* @__PURE__ */ jsxRuntime.jsx(
10634
+ "span",
10635
+ {
10636
+ className: "flex-1 truncate text-center",
10637
+ style: {
10638
+ minWidth: dims.minWidth,
10639
+ fontSize: dims.fontSize,
10640
+ lineHeight: `${dims.lineHeight}px`,
10641
+ color: disabled ? "var(--cmp-timepicker-text-disabled)" : hasValue ? "var(--cmp-timepicker-text-default)" : "var(--cmp-field-hint-color)"
10642
+ },
10643
+ children: hasValue ? displayText : placeholder
10644
+ }
10645
+ ),
10646
+ clearable && hasValue && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
10647
+ SGhostButton,
10648
+ {
10649
+ icon: "close",
10650
+ size: "xs",
10651
+ ariaLabel: "\uC2DC\uAC04 \uBC94\uC704 \uC9C0\uC6B0\uAE30",
10652
+ onClick: (event) => {
10653
+ event.stopPropagation();
10654
+ commitValue(null);
10655
+ }
10656
+ }
10657
+ )
10658
+ ]
10659
+ }
10660
+ ) }),
10661
+ /* @__PURE__ */ jsxRuntime.jsx(RPopover2__namespace.Portal, { container: portalContainer, children: /* @__PURE__ */ jsxRuntime.jsx(
10662
+ RPopover2__namespace.Content,
10663
+ {
10664
+ side: "bottom",
10665
+ align: "start",
10666
+ sideOffset: 4,
10667
+ className: cn("z-50", FLOATING_SLIDE_ANIM),
10668
+ children: /* @__PURE__ */ jsxRuntime.jsx(
10669
+ RangeTimeSelector,
10670
+ {
10671
+ value: normalized,
10672
+ useMeridiem: effectiveUseMeridiem,
10673
+ minuteStep,
10674
+ rangeOrder,
10675
+ onChange: commitValue
10676
+ }
10677
+ )
10678
+ }
10679
+ ) })
10680
+ ] })
10681
+ }
10682
+ );
10683
+ }
10684
+
9837
10685
  exports.BADGE_COLORS = BADGE_COLORS;
9838
10686
  exports.BUTTON_COLORS = BUTTON_COLORS;
9839
10687
  exports.BUTTON_SIZES = BUTTON_SIZES;
@@ -9891,6 +10739,8 @@ exports.STabs = STabs;
9891
10739
  exports.STag = STag;
9892
10740
  exports.STextLink = STextLink;
9893
10741
  exports.STextarea = STextarea;
10742
+ exports.STimePicker = STimePicker;
10743
+ exports.STimeRangePicker = STimeRangePicker;
9894
10744
  exports.SToast = SToast;
9895
10745
  exports.SToastContainer = SToastContainer;
9896
10746
  exports.SToggle = SToggle;
@@ -9898,6 +10748,8 @@ exports.STooltip = STooltip;
9898
10748
  exports.TAG_COLORS = TAG_COLORS;
9899
10749
  exports.TAG_SHAPES = TAG_SHAPES;
9900
10750
  exports.TAG_SIZES = TAG_SIZES;
10751
+ exports.TIMEPICKER_SIZES = TIMEPICKER_SIZES;
10752
+ exports.TIMEPICKER_SIZE_CONFIG = TIMEPICKER_SIZE_CONFIG;
9901
10753
  exports.buttonPreset = buttonPreset;
9902
10754
  exports.cn = cn;
9903
10755
  exports.findGnbAncestors = findGnbAncestors;