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