dialkit 0.2.1 → 0.2.2

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.d.ts CHANGED
@@ -33,10 +33,10 @@ type TextConfig = {
33
33
  };
34
34
  type DialValue = number | boolean | string | SpringConfig | ActionConfig | SelectConfig | ColorConfig | TextConfig;
35
35
  type DialConfig = {
36
- [key: string]: DialValue | [number, number, number] | DialConfig;
36
+ [key: string]: DialValue | [number, number, number, number?] | DialConfig;
37
37
  };
38
38
  type ResolvedValues<T extends DialConfig> = {
39
- [K in keyof T]: T[K] extends [number, number, number] ? number : T[K] extends SpringConfig ? SpringConfig : T[K] extends SelectConfig ? string : T[K] extends ColorConfig ? string : T[K] extends TextConfig ? string : T[K] extends DialConfig ? ResolvedValues<T[K]> : T[K];
39
+ [K in keyof T]: T[K] extends [number, number, number, number?] ? number : T[K] extends SpringConfig ? SpringConfig : T[K] extends SelectConfig ? string : T[K] extends ColorConfig ? string : T[K] extends TextConfig ? string : T[K] extends DialConfig ? ResolvedValues<T[K]> : T[K];
40
40
  };
41
41
  type ControlMeta = {
42
42
  type: 'slider' | 'toggle' | 'spring' | 'folder' | 'action' | 'select' | 'color' | 'text';
@@ -46,6 +46,7 @@ type ControlMeta = {
46
46
  max?: number;
47
47
  step?: number;
48
48
  children?: ControlMeta[];
49
+ defaultOpen?: boolean;
49
50
  options?: (string | {
50
51
  value: string;
51
52
  label: string;
package/dist/index.js CHANGED
@@ -160,16 +160,17 @@ var DialStoreClass = class {
160
160
  parseConfig(config, prefix) {
161
161
  const controls = [];
162
162
  for (const [key, value] of Object.entries(config)) {
163
+ if (key === "_collapsed") continue;
163
164
  const path = prefix ? `${prefix}.${key}` : key;
164
165
  const label = this.formatLabel(key);
165
- if (Array.isArray(value) && value.length === 3 && typeof value[0] === "number") {
166
+ if (Array.isArray(value) && value.length <= 4 && typeof value[0] === "number") {
166
167
  controls.push({
167
168
  type: "slider",
168
169
  path,
169
170
  label,
170
171
  min: value[1],
171
172
  max: value[2],
172
- step: this.inferStep(value[1], value[2])
173
+ step: value[3] ?? this.inferStep(value[1], value[2])
173
174
  });
174
175
  } else if (typeof value === "number") {
175
176
  const { min, max, step } = this.inferRange(value);
@@ -193,11 +194,14 @@ var DialStoreClass = class {
193
194
  controls.push({ type: "text", path, label });
194
195
  }
195
196
  } else if (typeof value === "object" && value !== null) {
197
+ const folderConfig = value;
198
+ const defaultOpen = "_collapsed" in folderConfig ? !folderConfig._collapsed : true;
196
199
  controls.push({
197
200
  type: "folder",
198
201
  path,
199
202
  label,
200
- children: this.parseConfig(value, path)
203
+ defaultOpen,
204
+ children: this.parseConfig(folderConfig, path)
201
205
  });
202
206
  }
203
207
  }
@@ -206,8 +210,9 @@ var DialStoreClass = class {
206
210
  flattenValues(config, prefix) {
207
211
  const values = {};
208
212
  for (const [key, value] of Object.entries(config)) {
213
+ if (key === "_collapsed") continue;
209
214
  const path = prefix ? `${prefix}.${key}` : key;
210
- if (Array.isArray(value) && value.length === 3 && typeof value[0] === "number") {
215
+ if (Array.isArray(value) && value.length <= 4 && typeof value[0] === "number") {
211
216
  values[path] = value[0];
212
217
  } else if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
213
218
  values[path] = value;
@@ -299,8 +304,9 @@ function useDialKit(name, config, options) {
299
304
  function buildResolvedValues(config, flatValues, prefix) {
300
305
  const result = {};
301
306
  for (const [key, configValue] of Object.entries(config)) {
307
+ if (key === "_collapsed") continue;
302
308
  const path = prefix ? `${prefix}.${key}` : key;
303
- if (Array.isArray(configValue) && configValue.length === 3 && typeof configValue[0] === "number") {
309
+ if (Array.isArray(configValue) && configValue.length <= 4 && typeof configValue[0] === "number") {
304
310
  result[key] = flatValues[path] ?? configValue[0];
305
311
  } else if (typeof configValue === "number" || typeof configValue === "boolean" || typeof configValue === "string") {
306
312
  result[key] = flatValues[path] ?? configValue;
@@ -346,7 +352,7 @@ function getFirstOptionValue(options) {
346
352
 
347
353
  // src/components/DialRoot.tsx
348
354
  import { useEffect as useEffect7, useState as useState8 } from "react";
349
- import { createPortal as createPortal2 } from "react-dom";
355
+ import { createPortal as createPortal3 } from "react-dom";
350
356
 
351
357
  // src/components/Panel.tsx
352
358
  import { useState as useState7, useSyncExternalStore as useSyncExternalStore3 } from "react";
@@ -464,10 +470,14 @@ var CLICK_THRESHOLD = 3;
464
470
  var DEAD_ZONE = 32;
465
471
  var MAX_CURSOR_RANGE = 200;
466
472
  var MAX_STRETCH = 8;
473
+ function decimalsForStep(step) {
474
+ const s = step.toString();
475
+ const dot = s.indexOf(".");
476
+ return dot === -1 ? 0 : s.length - dot - 1;
477
+ }
467
478
  function roundValue(val, step) {
468
- const decimals = step >= 1 ? 0 : 2;
469
- const factor = 10 ** decimals;
470
- return Math.round(val * factor) / factor;
479
+ const raw = Math.round(val / step) * step;
480
+ return parseFloat(raw.toFixed(decimalsForStep(step)));
471
481
  }
472
482
  function snapToDecile(rawValue, min, max) {
473
483
  const normalized = (rawValue - min) / (max - min);
@@ -615,7 +625,8 @@ function Slider({
615
625
  if (!isInteracting) return;
616
626
  if (isClickRef.current) {
617
627
  const rawValue = positionToValue(e.clientX);
618
- const snappedValue = snapToDecile(rawValue, min, max);
628
+ const discreteSteps2 = (max - min) / step;
629
+ const snappedValue = discreteSteps2 <= 10 ? Math.max(min, Math.min(max, min + Math.round((rawValue - min) / step) * step)) : snapToDecile(rawValue, min, max);
619
630
  const newPct = percentFromValue(snappedValue);
620
631
  if (animRef.current) {
621
632
  animRef.current.stop();
@@ -695,7 +706,7 @@ function Slider({
695
706
  e.stopPropagation();
696
707
  e.preventDefault();
697
708
  setShowInput(true);
698
- setInputValue(step >= 1 ? value.toFixed(0) : value.toFixed(2));
709
+ setInputValue(value.toFixed(decimalsForStep(step)));
699
710
  }
700
711
  };
701
712
  const handleInputKeyDown = (e) => {
@@ -709,7 +720,7 @@ function Slider({
709
720
  const handleInputBlur = () => {
710
721
  handleInputSubmit();
711
722
  };
712
- const displayValue = step >= 1 ? value.toFixed(0) : value.toFixed(2);
723
+ const displayValue = value.toFixed(decimalsForStep(step));
713
724
  const HANDLE_BUFFER = 8;
714
725
  const LABEL_CSS_LEFT = 10;
715
726
  const VALUE_CSS_RIGHT = 10;
@@ -727,7 +738,18 @@ function Slider({
727
738
  const valueDodge = percentage < leftThreshold || percentage > rightThreshold;
728
739
  const handleOpacity = !isActive ? 0 : valueDodge ? 0.1 : isDragging ? 0.9 : 0.5;
729
740
  const fillBackground = isActive ? "rgba(255, 255, 255, 0.15)" : "rgba(255, 255, 255, 0.11)";
730
- const hashMarks = Array.from({ length: 9 }, (_, i) => {
741
+ const discreteSteps = (max - min) / step;
742
+ const hashMarks = discreteSteps <= 10 ? Array.from({ length: discreteSteps - 1 }, (_, i) => {
743
+ const pct = (i + 1) * step / (max - min) * 100;
744
+ return /* @__PURE__ */ jsx2(
745
+ "div",
746
+ {
747
+ className: "dialkit-slider-hashmark",
748
+ style: { left: `${pct}%` }
749
+ },
750
+ i
751
+ );
752
+ }) : Array.from({ length: 9 }, (_, i) => {
731
753
  const pct = (i + 1) * 10;
732
754
  return /* @__PURE__ */ jsx2(
733
755
  "div",
@@ -1116,7 +1138,8 @@ function TextControl({ label, value, onChange, placeholder }) {
1116
1138
  }
1117
1139
 
1118
1140
  // src/components/SelectControl.tsx
1119
- import { useState as useState4, useRef as useRef5, useEffect as useEffect4 } from "react";
1141
+ import { useState as useState4, useRef as useRef5, useEffect as useEffect4, useCallback as useCallback2 } from "react";
1142
+ import { createPortal } from "react-dom";
1120
1143
  import { motion as motion4, AnimatePresence as AnimatePresence2 } from "motion/react";
1121
1144
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1122
1145
  function toTitleCase(s) {
@@ -1129,23 +1152,50 @@ function normalizeOptions(options) {
1129
1152
  }
1130
1153
  function SelectControl({ label, value, options, onChange }) {
1131
1154
  const [isOpen, setIsOpen] = useState4(false);
1132
- const containerRef = useRef5(null);
1155
+ const triggerRef = useRef5(null);
1156
+ const dropdownRef = useRef5(null);
1157
+ const [portalTarget, setPortalTarget] = useState4(null);
1158
+ const [pos, setPos] = useState4(null);
1133
1159
  const normalized = normalizeOptions(options);
1134
1160
  const selectedOption = normalized.find((o) => o.value === value);
1161
+ const updatePos = useCallback2(() => {
1162
+ const el = triggerRef.current;
1163
+ if (!el) return;
1164
+ const rect = el.getBoundingClientRect();
1165
+ const dropdownHeight = 8 + normalized.length * 36;
1166
+ const spaceBelow = window.innerHeight - rect.bottom - 4;
1167
+ const above = spaceBelow < dropdownHeight && rect.top > spaceBelow;
1168
+ setPos({
1169
+ top: above ? rect.top - 4 : rect.bottom + 4,
1170
+ left: rect.left,
1171
+ width: rect.width,
1172
+ above
1173
+ });
1174
+ }, [normalized.length]);
1175
+ useEffect4(() => {
1176
+ const root = triggerRef.current?.closest(".dialkit-root");
1177
+ setPortalTarget(root ?? document.body);
1178
+ }, []);
1179
+ useEffect4(() => {
1180
+ if (!isOpen) return;
1181
+ updatePos();
1182
+ }, [isOpen, updatePos]);
1135
1183
  useEffect4(() => {
1136
1184
  if (!isOpen) return;
1137
1185
  const handleClick = (e) => {
1138
- if (containerRef.current && !containerRef.current.contains(e.target)) {
1186
+ const target = e.target;
1187
+ if (triggerRef.current && !triggerRef.current.contains(target) && dropdownRef.current && !dropdownRef.current.contains(target)) {
1139
1188
  setIsOpen(false);
1140
1189
  }
1141
1190
  };
1142
1191
  document.addEventListener("mousedown", handleClick);
1143
1192
  return () => document.removeEventListener("mousedown", handleClick);
1144
1193
  }, [isOpen]);
1145
- return /* @__PURE__ */ jsxs8("div", { ref: containerRef, className: "dialkit-select-row", children: [
1194
+ return /* @__PURE__ */ jsxs8("div", { className: "dialkit-select-row", children: [
1146
1195
  /* @__PURE__ */ jsxs8(
1147
1196
  "button",
1148
1197
  {
1198
+ ref: triggerRef,
1149
1199
  className: "dialkit-select-trigger",
1150
1200
  onClick: () => setIsOpen(!isOpen),
1151
1201
  "data-open": String(isOpen),
@@ -1172,29 +1222,39 @@ function SelectControl({ label, value, options, onChange }) {
1172
1222
  ]
1173
1223
  }
1174
1224
  ),
1175
- /* @__PURE__ */ jsx8(AnimatePresence2, { children: isOpen && /* @__PURE__ */ jsx8(
1176
- motion4.div,
1177
- {
1178
- className: "dialkit-select-dropdown",
1179
- initial: { opacity: 0, y: -8, scale: 0.95 },
1180
- animate: { opacity: 1, y: 0, scale: 1 },
1181
- exit: { opacity: 0, y: -8, scale: 0.95 },
1182
- transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1183
- children: normalized.map((option) => /* @__PURE__ */ jsx8(
1184
- "button",
1185
- {
1186
- className: "dialkit-select-option",
1187
- "data-selected": String(option.value === value),
1188
- onClick: () => {
1189
- onChange(option.value);
1190
- setIsOpen(false);
1191
- },
1192
- children: option.label
1225
+ portalTarget && createPortal(
1226
+ /* @__PURE__ */ jsx8(AnimatePresence2, { children: isOpen && pos && /* @__PURE__ */ jsx8(
1227
+ motion4.div,
1228
+ {
1229
+ ref: dropdownRef,
1230
+ className: "dialkit-select-dropdown",
1231
+ initial: { opacity: 0, y: pos.above ? 8 : -8, scale: 0.95 },
1232
+ animate: { opacity: 1, y: 0, scale: 1 },
1233
+ exit: { opacity: 0, y: pos.above ? 8 : -8, scale: 0.95 },
1234
+ transition: { type: "spring", visualDuration: 0.15, bounce: 0 },
1235
+ style: {
1236
+ position: "fixed",
1237
+ left: pos.left,
1238
+ width: pos.width,
1239
+ ...pos.above ? { bottom: window.innerHeight - pos.top, transformOrigin: "bottom" } : { top: pos.top, transformOrigin: "top" }
1193
1240
  },
1194
- option.value
1195
- ))
1196
- }
1197
- ) })
1241
+ children: normalized.map((option) => /* @__PURE__ */ jsx8(
1242
+ "button",
1243
+ {
1244
+ className: "dialkit-select-option",
1245
+ "data-selected": String(option.value === value),
1246
+ onClick: () => {
1247
+ onChange(option.value);
1248
+ setIsOpen(false);
1249
+ },
1250
+ children: option.label
1251
+ },
1252
+ option.value
1253
+ ))
1254
+ }
1255
+ ) }),
1256
+ portalTarget
1257
+ )
1198
1258
  ] });
1199
1259
  }
1200
1260
 
@@ -1277,8 +1337,8 @@ function expandShorthandHex(hex) {
1277
1337
  }
1278
1338
 
1279
1339
  // src/components/PresetManager.tsx
1280
- import { useState as useState6, useRef as useRef7, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1281
- import { createPortal } from "react-dom";
1340
+ import { useState as useState6, useRef as useRef7, useEffect as useEffect6, useCallback as useCallback3 } from "react";
1341
+ import { createPortal as createPortal2 } from "react-dom";
1282
1342
  import { motion as motion5, AnimatePresence as AnimatePresence3 } from "motion/react";
1283
1343
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1284
1344
  function PresetManager({ panelId, presets, activePresetId, onAdd }) {
@@ -1288,7 +1348,7 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1288
1348
  const [pos, setPos] = useState6({ top: 0, left: 0, width: 0 });
1289
1349
  const hasPresets = presets.length > 0;
1290
1350
  const activePreset = presets.find((p) => p.id === activePresetId);
1291
- const open = useCallback2(() => {
1351
+ const open = useCallback3(() => {
1292
1352
  if (!hasPresets) return;
1293
1353
  const rect = triggerRef.current?.getBoundingClientRect();
1294
1354
  if (rect) {
@@ -1296,8 +1356,8 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1296
1356
  }
1297
1357
  setIsOpen(true);
1298
1358
  }, [hasPresets]);
1299
- const close = useCallback2(() => setIsOpen(false), []);
1300
- const toggle = useCallback2(() => {
1359
+ const close = useCallback3(() => setIsOpen(false), []);
1360
+ const toggle = useCallback3(() => {
1301
1361
  if (isOpen) close();
1302
1362
  else open();
1303
1363
  }, [isOpen, open, close]);
@@ -1353,12 +1413,12 @@ function PresetManager({ panelId, presets, activePresetId, onAdd }) {
1353
1413
  ]
1354
1414
  }
1355
1415
  ),
1356
- createPortal(
1416
+ createPortal2(
1357
1417
  /* @__PURE__ */ jsx10(AnimatePresence3, { children: isOpen && /* @__PURE__ */ jsxs10(
1358
1418
  motion5.div,
1359
1419
  {
1360
1420
  ref: dropdownRef,
1361
- className: "dialkit-preset-dropdown",
1421
+ className: "dialkit-root dialkit-preset-dropdown",
1362
1422
  style: { position: "fixed", top: pos.top, left: pos.left, minWidth: pos.width },
1363
1423
  initial: { opacity: 0, y: 4, scale: 0.97 },
1364
1424
  animate: { opacity: 1, y: 0, scale: 1 },
@@ -1477,7 +1537,7 @@ Apply these values as the new defaults in the useDialKit call.`;
1477
1537
  control.path
1478
1538
  );
1479
1539
  case "folder":
1480
- return /* @__PURE__ */ jsx11(Folder, { title: control.label, children: control.children?.map(renderControl) }, control.path);
1540
+ return /* @__PURE__ */ jsx11(Folder, { title: control.label, defaultOpen: control.defaultOpen ?? true, children: control.children?.map(renderControl) }, control.path);
1481
1541
  case "text":
1482
1542
  return /* @__PURE__ */ jsx11(
1483
1543
  TextControl,
@@ -1638,7 +1698,7 @@ function DialRoot({ position = "top-right" }) {
1638
1698
  return null;
1639
1699
  }
1640
1700
  const content = /* @__PURE__ */ jsx12("div", { className: "dialkit-root", children: /* @__PURE__ */ jsx12("div", { className: "dialkit-panel", "data-position": position, children: panels.map((panel) => /* @__PURE__ */ jsx12(Panel, { panel }, panel.id)) }) });
1641
- return createPortal2(content, document.body);
1701
+ return createPortal3(content, document.body);
1642
1702
  }
1643
1703
 
1644
1704
  // src/components/ButtonGroup.tsx