@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.
- package/.github/workflows/publish.yml +66 -0
- package/.storybook/main.ts +1 -1
- package/.storybook/preview.tsx +5 -1
- package/CLAUDE.md +25 -0
- package/README.md +79 -0
- package/bun.lock +211 -202
- package/eslint.config.mjs +85 -84
- package/package.json +21 -20
- package/roadmap.md +27 -0
- package/src/components/Chat/ChatComposer/ChatComposer.stories.tsx +1 -1
- package/src/components/Chat/ChatMessage/ChatMessage.stories.tsx +1 -1
- package/src/components/Chat/ChatRecipientsHeader/ChatRecipientsHeader.stories.tsx +1 -1
- package/src/components/Chat/ChatShell/ChatShell.stories.tsx +1 -1
- package/src/components/Chat/PillCombobox/PillCombobox.stories.tsx +1 -1
- package/src/components/Content/Badge/Badge.stories.tsx +1 -1
- package/src/components/Content/Card/Card.stories.tsx +1 -1
- package/src/components/Content/EditableMarkdown/EditableMarkdown.stories.tsx +1 -1
- package/src/components/Content/Heading/Heading.stories.tsx +1 -1
- package/src/components/Content/Link/Link.stories.tsx +1 -1
- package/src/components/Content/List/List.stories.tsx +1 -1
- package/src/components/Content/LoadingContainer/LoadingContainer.stories.tsx +1 -1
- package/src/components/Content/Markdown/Markdown.stories.tsx +1 -1
- package/src/components/Content/Menu/Menu.stories.tsx +1 -1
- package/src/components/Content/Text/Text.stories.tsx +1 -1
- package/src/components/Forms/Button/Button.stories.tsx +1 -1
- package/src/components/Forms/Field/Field.stories.tsx +1 -1
- package/src/components/Forms/IconButton/IconButton.stories.tsx +1 -1
- package/src/components/Forms/Input/Input.stories.tsx +1 -1
- package/src/components/Forms/Select/MultiSelect/MultiSelect.stories.tsx +1 -1
- package/src/components/Forms/Select/SingleSelect/SingleSelect.stories.tsx +1 -1
- package/src/components/Forms/Textarea/Textarea.stories.tsx +1 -1
- package/src/components/Json/Json/Json.stories.tsx +1 -1
- package/src/components/Json/JsonTable/JsonTable.stories.tsx +1 -1
- package/src/components/Layout/Bar/Bar.stories.tsx +1 -1
- package/src/components/Layout/Debug/Debug.stories.tsx +1 -1
- package/src/components/Layout/Divider/Divider.stories.tsx +1 -1
- package/src/components/Layout/Grid/Grid.stories.tsx +1 -1
- package/src/components/Layout/Panels/Panels.stories.tsx +47 -1
- package/src/components/Layout/Panels/index.tsx +17 -1
- package/src/components/Layout/Panels/types.ts +5 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +1 -1
- package/src/components/Modals/ConfirmModal/ConfirmModal.stories.tsx +1 -1
- package/src/components/Modals/LargeModal/LargeModal.stories.tsx +1 -1
- package/src/components/Modals/MediumModal/MediumModal.stories.tsx +1 -1
- package/src/components/Modals/MediumModal/MediumModal.test.tsx +11 -11
- package/src/components/Navigation/TabBar/TabBar.stories.tsx +1 -1
- package/src/components/Navigation/VerticalNav/VerticalNav.stories.tsx +1 -1
- package/src/components/Overlays/Popover/Popover.stories.tsx +1 -1
- package/src/components/Overlays/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/components/Primitives/EmptyValue/EmptyValue.stories.tsx +1 -1
- package/src/components/Primitives/LinedStack/LinedStack.stories.tsx +1 -1
- package/src/components/Primitives/LongText/LongText.stories.tsx +1 -1
- package/src/components/Primitives/Num/Num.stories.tsx +1 -1
- package/src/components/Primitives/Percent/Percent.stories.tsx +1 -1
- package/src/components/Primitives/RelativeTime/RelativeTime.stories.tsx +1 -1
- package/src/components/Tables/BigTable/BigTable.stories.tsx +1 -1
- package/src/components/Tables/QuickTable/QuickTable.stories.tsx +1 -1
- package/src/forms/CLAUDE.md +144 -0
- package/src/forms/path/path.ts +50 -0
- package/src/forms/path/types.test-d.ts +175 -0
- package/src/forms/path/types.ts +35 -0
- package/src/forms/useFormState/types.ts +13 -0
- package/src/forms/useFormState/useFormState.ts +14 -0
- package/src/forms/validations/types.test-d.ts +26 -0
- package/src/forms/validations/types.ts +15 -0
- package/src/hooks/useClickOutside/index.ts +57 -0
- package/src/hooks/useStableCallback/index.ts +36 -0
- package/src/index.ts +2 -0
- package/src/storybook/Composition.stories.tsx +1 -1
- 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 { Lorem, Placeholder, Toggle, Repeat } from './index';
|
|
3
3
|
import { Button } from '../components/Forms/Button';
|
|
4
4
|
import { Stack } from '../components/Layout/Stack';
|