@structuralists/scaffolding 0.0.1 → 0.1.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.
Files changed (70) hide show
  1. package/.github/workflows/publish.yml +66 -0
  2. package/.storybook/main.ts +1 -1
  3. package/.storybook/preview.tsx +5 -1
  4. package/CLAUDE.md +25 -0
  5. package/README.md +79 -0
  6. package/bun.lock +211 -202
  7. package/eslint.config.mjs +85 -84
  8. package/package.json +21 -20
  9. package/roadmap.md +27 -0
  10. package/src/components/Chat/ChatComposer/ChatComposer.stories.tsx +1 -1
  11. package/src/components/Chat/ChatMessage/ChatMessage.stories.tsx +1 -1
  12. package/src/components/Chat/ChatRecipientsHeader/ChatRecipientsHeader.stories.tsx +1 -1
  13. package/src/components/Chat/ChatShell/ChatShell.stories.tsx +1 -1
  14. package/src/components/Chat/PillCombobox/PillCombobox.stories.tsx +1 -1
  15. package/src/components/Content/Badge/Badge.stories.tsx +1 -1
  16. package/src/components/Content/Card/Card.stories.tsx +1 -1
  17. package/src/components/Content/EditableMarkdown/EditableMarkdown.stories.tsx +1 -1
  18. package/src/components/Content/Heading/Heading.stories.tsx +1 -1
  19. package/src/components/Content/Link/Link.stories.tsx +1 -1
  20. package/src/components/Content/List/List.stories.tsx +1 -1
  21. package/src/components/Content/LoadingContainer/LoadingContainer.stories.tsx +1 -1
  22. package/src/components/Content/Markdown/Markdown.stories.tsx +1 -1
  23. package/src/components/Content/Menu/Menu.stories.tsx +1 -1
  24. package/src/components/Content/Text/Text.stories.tsx +1 -1
  25. package/src/components/Forms/Button/Button.stories.tsx +1 -1
  26. package/src/components/Forms/Field/Field.stories.tsx +1 -1
  27. package/src/components/Forms/IconButton/IconButton.stories.tsx +1 -1
  28. package/src/components/Forms/Input/Input.stories.tsx +1 -1
  29. package/src/components/Forms/Select/MultiSelect/MultiSelect.stories.tsx +1 -1
  30. package/src/components/Forms/Select/SingleSelect/SingleSelect.stories.tsx +1 -1
  31. package/src/components/Forms/Textarea/Textarea.stories.tsx +1 -1
  32. package/src/components/Json/Json/Json.stories.tsx +1 -1
  33. package/src/components/Json/JsonTable/JsonTable.stories.tsx +1 -1
  34. package/src/components/Layout/Bar/Bar.stories.tsx +1 -1
  35. package/src/components/Layout/Debug/Debug.stories.tsx +1 -1
  36. package/src/components/Layout/Divider/Divider.stories.tsx +1 -1
  37. package/src/components/Layout/Grid/Grid.stories.tsx +1 -1
  38. package/src/components/Layout/Panels/Panels.stories.tsx +47 -1
  39. package/src/components/Layout/Panels/index.tsx +17 -1
  40. package/src/components/Layout/Panels/types.ts +5 -0
  41. package/src/components/Layout/Stack/Stack.stories.tsx +1 -1
  42. package/src/components/Modals/ConfirmModal/ConfirmModal.stories.tsx +1 -1
  43. package/src/components/Modals/LargeModal/LargeModal.stories.tsx +1 -1
  44. package/src/components/Modals/MediumModal/MediumModal.stories.tsx +1 -1
  45. package/src/components/Modals/MediumModal/MediumModal.test.tsx +11 -11
  46. package/src/components/Navigation/TabBar/TabBar.stories.tsx +1 -1
  47. package/src/components/Navigation/VerticalNav/VerticalNav.stories.tsx +1 -1
  48. package/src/components/Overlays/Popover/Popover.stories.tsx +1 -1
  49. package/src/components/Overlays/Tooltip/Tooltip.stories.tsx +1 -1
  50. package/src/components/Primitives/EmptyValue/EmptyValue.stories.tsx +1 -1
  51. package/src/components/Primitives/LinedStack/LinedStack.stories.tsx +1 -1
  52. package/src/components/Primitives/LongText/LongText.stories.tsx +1 -1
  53. package/src/components/Primitives/Num/Num.stories.tsx +1 -1
  54. package/src/components/Primitives/Percent/Percent.stories.tsx +1 -1
  55. package/src/components/Primitives/RelativeTime/RelativeTime.stories.tsx +1 -1
  56. package/src/components/Tables/BigTable/BigTable.stories.tsx +1 -1
  57. package/src/components/Tables/QuickTable/QuickTable.stories.tsx +1 -1
  58. package/src/forms/CLAUDE.md +144 -0
  59. package/src/forms/path/path.ts +50 -0
  60. package/src/forms/path/types.test-d.ts +175 -0
  61. package/src/forms/path/types.ts +35 -0
  62. package/src/forms/useFormState/types.ts +13 -0
  63. package/src/forms/useFormState/useFormState.ts +14 -0
  64. package/src/forms/validations/types.test-d.ts +26 -0
  65. package/src/forms/validations/types.ts +15 -0
  66. package/src/hooks/useClickOutside/index.ts +57 -0
  67. package/src/hooks/useStableCallback/index.ts +36 -0
  68. package/src/index.ts +2 -0
  69. package/src/storybook/Composition.stories.tsx +1 -1
  70. package/src/storybook/_StoryUtils.stories.tsx +1 -1
@@ -0,0 +1,57 @@
1
+ import { useEffect, useRef, type RefObject } from 'react';
2
+ import { useStableCallback } from '../useStableCallback';
3
+
4
+ type UseClickOutsideArgs<Element extends HTMLElement> = {
5
+ /** Optional external ref. When omitted, the hook owns an internal ref —
6
+ * attach the returned `ref` to the element you want to guard. */
7
+ ref?: RefObject<Element | null>;
8
+ /** Callback fired on a `pointerdown` outside the guarded element. When
9
+ * `undefined`, the listener is detached (lets callers gate via a prop
10
+ * without conditionally calling the hook). */
11
+ onOutside: (() => void) | undefined;
12
+ /** Detach the listener when false. Defaults to true. */
13
+ enabled?: boolean;
14
+ };
15
+
16
+ type UseClickOutsideReturn<Element extends HTMLElement> = {
17
+ /** Mirror of the passed `ref`, or an internal ref when none was passed.
18
+ * Attach this to the element to be guarded. */
19
+ ref: RefObject<Element | null>;
20
+ };
21
+
22
+ /** Calls `onOutside` when a `pointerdown` lands outside the guarded
23
+ * element. `pointerdown` is used so dismissal precedes focus shifts and
24
+ * text-selection starts on the underlying target.
25
+ *
26
+ * The callback is internally stabilized, so callers can pass an inline
27
+ * function without retriggering the listener attach/detach effect.
28
+ *
29
+ * Note: clicks inside portal'd descendants of the guarded element
30
+ * (popovers, tooltips, menus) count as outside since they are not
31
+ * DOM-contained. Compose with additional refs or a custom predicate
32
+ * if that is not desired. */
33
+ export const useClickOutside = <Element extends HTMLElement = HTMLElement>(
34
+ args: UseClickOutsideArgs<Element>,
35
+ ): UseClickOutsideReturn<Element> => {
36
+ const { ref: passedRef, onOutside, enabled = true } = args;
37
+
38
+ const innerRef = useRef<Element | null>(null);
39
+ const ref = passedRef ?? innerRef;
40
+
41
+ const stableOnOutside = useStableCallback({ callback: onOutside });
42
+ const isActive = enabled && onOutside != null;
43
+
44
+ useEffect(() => {
45
+ if (!isActive) return;
46
+ const handler = (event: PointerEvent) => {
47
+ const el = ref.current;
48
+ if (el == null) return;
49
+ if (event.target instanceof Node && el.contains(event.target)) return;
50
+ stableOnOutside();
51
+ };
52
+ document.addEventListener('pointerdown', handler);
53
+ return () => document.removeEventListener('pointerdown', handler);
54
+ }, [ref, isActive, stableOnOutside]);
55
+
56
+ return { ref };
57
+ };
@@ -0,0 +1,36 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ type UseStableCallbackArgs<Args extends readonly unknown[], Return> = {
4
+ callback: ((...args: Args) => Return) | undefined;
5
+ };
6
+
7
+ /** Returns a function reference that is stable for the component's lifetime
8
+ * but always invokes the latest `callback`. Use this to keep effect dep
9
+ * arrays from re-firing when a parent passes an inline callback prop.
10
+ *
11
+ * The wrapper is held in `useState` (not `useCallback`) because React may
12
+ * discard a `useCallback` memo at any time, which would silently break
13
+ * identity-based dependency tracking; `useState`'s initial value is
14
+ * guaranteed stable for the mount's lifetime.
15
+ *
16
+ * The latest callback is committed via `useEffect`, so the wrapper still
17
+ * reflects the previous value during a given render — call it from event
18
+ * handlers and effects, not inline during render. */
19
+ export const useStableCallback = <Args extends readonly unknown[], Return>(
20
+ args: UseStableCallbackArgs<Args, Return>,
21
+ ): ((...args: Args) => Return | undefined) => {
22
+ const { callback } = args;
23
+ const callbackRef = useRef(callback);
24
+
25
+ useEffect(() => {
26
+ callbackRef.current = callback;
27
+ }, [callback]);
28
+
29
+ const [stableFn] = useState(
30
+ () =>
31
+ (...callArgs: Args): Return | undefined =>
32
+ callbackRef.current?.(...callArgs),
33
+ );
34
+
35
+ return stableFn;
36
+ };
package/src/index.ts CHANGED
@@ -85,3 +85,5 @@ export { RelativeTime, type RelativeTimeProps } from './components/Primitives/Re
85
85
  export { LinedStack, type LinedStackProps } from './components/Primitives/LinedStack';
86
86
  export { LongText, type LongTextProps } from './components/Primitives/LongText';
87
87
  export { type BackgroundToken } from './tokens';
88
+ export { useStableCallback } from './hooks/useStableCallback';
89
+ export { useClickOutside } from './hooks/useClickOutside';
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { useRef, useState } from 'react';
3
3
  import { Button } from '../components/Forms/Button';
4
4
  import { Field } from '../components/Forms/Field';
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { Lorem, Placeholder, Toggle, Repeat } from './index';
3
3
  import { Button } from '../components/Forms/Button';
4
4
  import { Stack } from '../components/Layout/Stack';