@astryxdesign/lab 0.1.9-canary.d2d84f8 → 0.1.9-canary.e00a241

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 (40) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +95 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
  3. package/dist/BottomSheet/BottomSheet.js +150 -0
  4. package/dist/BottomSheet/index.d.ts +9 -0
  5. package/dist/BottomSheet/index.d.ts.map +1 -0
  6. package/dist/BottomSheet/index.js +10 -0
  7. package/dist/BottomSheet/useSheetGestures.d.ts +69 -0
  8. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
  9. package/dist/BottomSheet/useSheetGestures.js +163 -0
  10. package/dist/RichTextEditor/RichTextEditor.d.ts +67 -6
  11. package/dist/RichTextEditor/RichTextEditor.d.ts.map +1 -1
  12. package/dist/RichTextEditor/RichTextEditor.js +165 -12
  13. package/dist/RichTextEditor/editorTheme.d.ts.map +1 -1
  14. package/dist/RichTextEditor/editorTheme.js +33 -7
  15. package/dist/RichTextEditor/index.d.ts +1 -1
  16. package/dist/RichTextEditor/index.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/lab.css +23 -0
  21. package/package.json +2 -2
  22. package/src/BottomSheet/BottomSheet.doc.mjs +105 -0
  23. package/src/BottomSheet/BottomSheet.test.tsx +233 -0
  24. package/src/BottomSheet/BottomSheet.tsx +226 -0
  25. package/src/BottomSheet/index.ts +11 -0
  26. package/src/BottomSheet/useSheetGestures.doc.mjs +31 -0
  27. package/src/BottomSheet/useSheetGestures.test.ts +160 -0
  28. package/src/BottomSheet/useSheetGestures.ts +226 -0
  29. package/src/ChatReasoning/ChatReasoning.doc.mjs +49 -2
  30. package/src/CodeEditor/CodeEditor.doc.mjs +67 -2
  31. package/src/LogStream/LogStream.doc.mjs +53 -2
  32. package/src/RichTextEditor/RichTextEditor.doc.mjs +19 -1
  33. package/src/RichTextEditor/RichTextEditor.test.tsx +441 -11
  34. package/src/RichTextEditor/RichTextEditor.tsx +289 -31
  35. package/src/RichTextEditor/editorTheme.ts +63 -3
  36. package/src/RichTextEditor/index.ts +2 -0
  37. package/src/Schedule/Schedule.doc.mjs +143 -3
  38. package/src/Stepper/Stepper.doc.mjs +6 -0
  39. package/src/__tests__/labApiContractDrift.test.tsx +57 -0
  40. package/src/index.ts +5 -0
@@ -0,0 +1,226 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useSheetGestures.ts
7
+ * @input Uses React (useCallback, useEffect, useMemo, useRef, useState)
8
+ * @output Exports useSheetGestures hook and its option/result types
9
+ * @position Internal to BottomSheet; not exported from the lab entry point
10
+ *
11
+ * Drag-to-dismiss machinery for the bottom sheet. Tracks a pointer drag
12
+ * down the block axis, translates the sliding surface live, and on release
13
+ * either dismisses (past a distance or velocity threshold) or springs back
14
+ * to fully open. Pointer-events based so one code path covers mouse + touch.
15
+ *
16
+ * Kept private to BottomSheet: the behavior is sheet-specific (a dismiss
17
+ * edge on a bottom-anchored surface). It is not a general primitive and is
18
+ * intentionally not exported.
19
+ *
20
+ * SSR-safe: no window/document access at module scope; all measurement
21
+ * happens inside handlers. Respects prefers-reduced-motion by skipping the
22
+ * settle transition.
23
+ *
24
+ * SYNC: When modified, update these files to stay in sync:
25
+ * - /packages/lab/src/BottomSheet/useSheetGestures.doc.mjs
26
+ * - /packages/lab/src/BottomSheet/useSheetGestures.test.ts
27
+ */
28
+
29
+ import {
30
+ useCallback,
31
+ useEffect,
32
+ useMemo,
33
+ useRef,
34
+ useState,
35
+ type CSSProperties,
36
+ type PointerEvent as ReactPointerEvent,
37
+ } from 'react';
38
+
39
+ // Fraction of the measured sheet height a drag must cross (with low
40
+ // velocity) before release dismisses instead of springing back.
41
+ const DISMISS_DISTANCE_RATIO = 0.25;
42
+ // A quick flick past this speed (px/ms) dismisses regardless of distance.
43
+ const DISMISS_VELOCITY = 0.5;
44
+
45
+ export interface UseSheetGesturesOptions {
46
+ /** Whether the owning sheet is open. Drag state resets when it closes. */
47
+ isOpen: boolean;
48
+ /** Called when a drag crosses the dismiss threshold (distance or flick). */
49
+ onDismiss: () => void;
50
+ }
51
+
52
+ export interface SheetContentProps {
53
+ style: CSSProperties;
54
+ }
55
+
56
+ export interface SheetHandleProps {
57
+ style: CSSProperties;
58
+ onPointerDown: (event: ReactPointerEvent) => void;
59
+ onPointerMove: (event: ReactPointerEvent) => void;
60
+ onPointerUp: (event: ReactPointerEvent) => void;
61
+ onPointerCancel: (event: ReactPointerEvent) => void;
62
+ }
63
+
64
+ export interface UseSheetGesturesResult {
65
+ /** Spread on the sliding surface: live translate + touch-action guard. */
66
+ contentProps: SheetContentProps;
67
+ /** Spread on the grab-handle element: pointer drag handlers. */
68
+ handleProps: SheetHandleProps;
69
+ /** Current live drag offset in px (positive = dragged toward the edge). */
70
+ dragOffset: number;
71
+ /** Whether a drag is currently in progress. */
72
+ isDragging: boolean;
73
+ }
74
+
75
+ function prefersReducedMotion(): boolean {
76
+ if (typeof window === 'undefined' || !window.matchMedia) {
77
+ return false;
78
+ }
79
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
80
+ }
81
+
82
+ /**
83
+ * Drag-to-dismiss machinery for the bottom sheet. Returns props to spread on
84
+ * the grab handle and the sliding surface, plus live drag state for callers
85
+ * that want to drive their own animation.
86
+ *
87
+ * @example
88
+ * ```
89
+ * const {contentProps, handleProps} = useSheetGestures({
90
+ * isOpen,
91
+ * onDismiss: () => onOpenChange(false),
92
+ * });
93
+ * <div {...contentProps}>
94
+ * <div {...handleProps} />
95
+ * {children}
96
+ * </div>
97
+ * ```
98
+ */
99
+ export function useSheetGestures({
100
+ isOpen,
101
+ onDismiss,
102
+ }: UseSheetGesturesOptions): UseSheetGesturesResult {
103
+ const [dragOffset, setDragOffset] = useState(0);
104
+ const [isDragging, setIsDragging] = useState(false);
105
+
106
+ // Latest callback ref so pointer handlers stay stable across renders.
107
+ const onDismissRef = useRef(onDismiss);
108
+ useEffect(() => {
109
+ onDismissRef.current = onDismiss;
110
+ });
111
+
112
+ // Live drag bookkeeping (refs so pointermove doesn't churn renders).
113
+ const dragStateRef = useRef<{
114
+ pointerId: number;
115
+ startCoord: number;
116
+ lastCoord: number;
117
+ lastTime: number;
118
+ velocity: number;
119
+ height: number;
120
+ } | null>(null);
121
+
122
+ // Reset drag state each time the sheet re-opens.
123
+ useEffect(() => {
124
+ if (isOpen) {
125
+ setDragOffset(0);
126
+ setIsDragging(false);
127
+ }
128
+ }, [isOpen]);
129
+
130
+ const measureHeight = useCallback((el: HTMLElement | null): number => {
131
+ const sheet = el?.closest<HTMLElement>('[data-astryx-sheet]') ?? el;
132
+ return sheet?.getBoundingClientRect().height ?? 0;
133
+ }, []);
134
+
135
+ const settleFromDrag = useCallback(
136
+ (offset: number, velocity: number, height: number) => {
137
+ const dismiss =
138
+ offset > height * DISMISS_DISTANCE_RATIO || velocity > DISMISS_VELOCITY;
139
+ if (dismiss) {
140
+ onDismissRef.current();
141
+ return;
142
+ }
143
+ // Below threshold: spring back to fully open.
144
+ setDragOffset(0);
145
+ },
146
+ [],
147
+ );
148
+
149
+ const handlePointerDown = useCallback(
150
+ (event: ReactPointerEvent) => {
151
+ const target = event.currentTarget as HTMLElement;
152
+ target.setPointerCapture?.(event.pointerId);
153
+ dragStateRef.current = {
154
+ pointerId: event.pointerId,
155
+ startCoord: event.clientY,
156
+ lastCoord: event.clientY,
157
+ lastTime: event.timeStamp,
158
+ velocity: 0,
159
+ height: measureHeight(target),
160
+ };
161
+ setIsDragging(true);
162
+ },
163
+ [measureHeight],
164
+ );
165
+
166
+ const handlePointerMove = useCallback((event: ReactPointerEvent) => {
167
+ const state = dragStateRef.current;
168
+ if (!state || state.pointerId !== event.pointerId) {
169
+ return;
170
+ }
171
+ // Dragging DOWN (increasing Y) collapses/dismisses. Only allow drag in
172
+ // the closing direction past the fully-open position (no overscroll).
173
+ const delta = event.clientY - state.startCoord;
174
+ const dt = event.timeStamp - state.lastTime;
175
+ if (dt > 0) {
176
+ state.velocity = (event.clientY - state.lastCoord) / dt;
177
+ state.lastCoord = event.clientY;
178
+ state.lastTime = event.timeStamp;
179
+ }
180
+ setDragOffset(Math.max(0, delta));
181
+ }, []);
182
+
183
+ const endDrag = useCallback(
184
+ (event: ReactPointerEvent) => {
185
+ const state = dragStateRef.current;
186
+ if (!state || state.pointerId !== event.pointerId) {
187
+ return;
188
+ }
189
+ const target = event.currentTarget as HTMLElement;
190
+ target.releasePointerCapture?.(event.pointerId);
191
+ const offset = Math.max(0, event.clientY - state.startCoord);
192
+ dragStateRef.current = null;
193
+ setIsDragging(false);
194
+ settleFromDrag(offset, state.velocity, state.height || 1);
195
+ },
196
+ [settleFromDrag],
197
+ );
198
+
199
+ // Reduced motion skips the settle transition (snaps instantly).
200
+ const reducedMotion = useMemo(prefersReducedMotion, [isOpen]);
201
+
202
+ const contentProps = useMemo<SheetContentProps>(
203
+ () => ({
204
+ style: {
205
+ transform: dragOffset !== 0 ? `translateY(${dragOffset}px)` : undefined,
206
+ transition: isDragging || reducedMotion ? 'none' : undefined,
207
+ touchAction: 'none',
208
+ overscrollBehavior: 'contain',
209
+ },
210
+ }),
211
+ [dragOffset, isDragging, reducedMotion],
212
+ );
213
+
214
+ const handleProps = useMemo<SheetHandleProps>(
215
+ () => ({
216
+ style: {touchAction: 'none', cursor: 'grab'},
217
+ onPointerDown: handlePointerDown,
218
+ onPointerMove: handlePointerMove,
219
+ onPointerUp: endDrag,
220
+ onPointerCancel: endDrag,
221
+ }),
222
+ [handlePointerDown, handlePointerMove, endDrag],
223
+ );
224
+
225
+ return {contentProps, handleProps, dragOffset, isDragging};
226
+ }
@@ -7,10 +7,57 @@ export const docs = {
7
7
  displayName: 'Chat Reasoning',
8
8
  group: 'Chat',
9
9
  category: 'Chat',
10
+ keywords: ['reasoning', 'thinking', 'thought', 'chain-of-thought', 'chat', 'llm', 'ai'],
10
11
 
11
12
  usage: {
12
- description: '',
13
+ description:
14
+ 'Compact collapsible container for displaying model reasoning or chain-of-thought details. Shows a single-line summary when collapsed and expands to reveal full reasoning text.',
13
15
  },
14
16
 
15
- props: [],
17
+ props: [
18
+ {
19
+ name: 'children',
20
+ type: 'ReactNode',
21
+ description: 'Reasoning content string or custom ReactNode.',
22
+ required: true,
23
+ },
24
+ {
25
+ name: 'label',
26
+ type: 'string',
27
+ description: 'Header label displayed in the trigger line.',
28
+ default: "'Thinking'",
29
+ },
30
+ {
31
+ name: 'duration',
32
+ type: 'string',
33
+ description: 'Duration string shown next to the label (e.g. "12s").',
34
+ },
35
+ {
36
+ name: 'isStreaming',
37
+ type: 'boolean',
38
+ description: 'Whether reasoning content is actively streaming. Displays animated shimmer effect.',
39
+ default: 'false',
40
+ },
41
+ {
42
+ name: 'isExpanded',
43
+ type: 'boolean',
44
+ description: 'Controlled expanded state.',
45
+ },
46
+ {
47
+ name: 'defaultIsExpanded',
48
+ type: 'boolean',
49
+ description: 'Default expanded state for uncontrolled usage.',
50
+ default: 'false',
51
+ },
52
+ {
53
+ name: 'onExpandedChange',
54
+ type: '(isExpanded: boolean) => void',
55
+ description: 'Callback fired when the expanded state changes.',
56
+ },
57
+ {
58
+ name: 'xstyle',
59
+ type: 'StyleXStyles',
60
+ description: 'StyleX styles for layout customization.',
61
+ },
62
+ ],
16
63
  };
@@ -7,10 +7,75 @@ export const docs = {
7
7
  displayName: 'Code Editor',
8
8
  group: 'CodeEditor',
9
9
  category: 'Data Input',
10
+ keywords: ['code', 'editor', 'syntax', 'highlight', 'input', 'textarea'],
10
11
 
11
12
  usage: {
12
- description: '',
13
+ description:
14
+ 'Editable code component with real-time syntax highlighting using the CSS Custom Highlight API. Supports line numbers, read-only state, placeholder text, and custom tokenizers.',
13
15
  },
14
16
 
15
- props: [],
17
+ props: [
18
+ {
19
+ name: 'value',
20
+ type: 'string',
21
+ description: 'Controlled string value for the editor.',
22
+ required: true,
23
+ },
24
+ {
25
+ name: 'onChange',
26
+ type: '(value: string) => void',
27
+ description: 'Callback fired when the code content changes.',
28
+ required: true,
29
+ },
30
+ {
31
+ name: 'label',
32
+ type: 'string',
33
+ description: 'Accessible label for the editable region (aria-label).',
34
+ required: true,
35
+ },
36
+ {
37
+ name: 'language',
38
+ type: 'string',
39
+ description: 'Programming language for syntax highlighting.',
40
+ default: "'plaintext'",
41
+ },
42
+ {
43
+ name: 'hasLineNumbers',
44
+ type: 'boolean',
45
+ description: 'Display line number gutter on the left.',
46
+ default: 'false',
47
+ },
48
+ {
49
+ name: 'isReadOnly',
50
+ type: 'boolean',
51
+ description: 'Renders the editor in a read-only state.',
52
+ default: 'false',
53
+ },
54
+ {
55
+ name: 'placeholder',
56
+ type: 'string',
57
+ description: 'Placeholder text displayed when value is empty.',
58
+ },
59
+ {
60
+ name: 'maxHeight',
61
+ type: 'number | string',
62
+ description: 'Maximum container height before internal scrolling.',
63
+ },
64
+ {
65
+ name: 'size',
66
+ type: "'sm' | 'md'",
67
+ description: 'Font size variant for the code text.',
68
+ default: "'md'",
69
+ },
70
+ {
71
+ name: 'tokenizer',
72
+ type: '(code: string, language: string) => TokenLine[]',
73
+ description: 'Custom tokenizer function override for language parsing.',
74
+ },
75
+ {
76
+ name: 'xstyle',
77
+ type: 'StyleXStyles',
78
+ description: 'StyleX styles for layout customization.',
79
+ },
80
+ ],
16
81
  };
@@ -7,11 +7,62 @@ export const docs = {
7
7
  displayName: 'Log Stream',
8
8
  group: 'LogStream',
9
9
  category: 'Content',
10
+ keywords: ['log', 'stream', 'terminal', 'console', 'output', 'logs', 'tail'],
10
11
 
11
12
  usage: {
12
13
  description:
13
- 'Experimental streaming log viewer: mono grid rows (timestamp | level | source | message) with token-derived level accents, expandable per-row detail panels, follow-scroll live tailing with a "Jump to latest" affordance, and an always-dark terminal variant. Appended rows fade in via @starting-style (instant under prefers-reduced-motion). No virtualization; window streams beyond a few thousand rows in the caller.',
14
+ 'Experimental streaming log viewer: mono grid rows (timestamp | level | source | message) with token-derived level accents, expandable per-row detail panels, follow-scroll live tailing with a "Jump to latest" affordance, and an always-dark terminal variant. Appended rows fade in via @starting-style.',
14
15
  },
15
16
 
16
- props: [],
17
+ props: [
18
+ {
19
+ name: 'entries',
20
+ type: 'LogEntry[]',
21
+ description: 'Array of log entry objects to render.',
22
+ required: true,
23
+ },
24
+ {
25
+ name: 'variant',
26
+ type: "'default' | 'terminal'",
27
+ description: "Visual variant style: 'default' uses theme surface background, 'terminal' uses fixed dark background.",
28
+ default: "'default'",
29
+ },
30
+ {
31
+ name: 'isFollowing',
32
+ type: 'boolean',
33
+ description: 'Controlled follow-pinning state to auto-scroll to the newest entry.',
34
+ },
35
+ {
36
+ name: 'onFollowChange',
37
+ type: '(following: boolean) => void',
38
+ description: 'Callback fired when follow-pinning state changes.',
39
+ },
40
+ {
41
+ name: 'maxHeight',
42
+ type: 'number | string',
43
+ description: 'Maximum container height before vertical scrolling.',
44
+ },
45
+ {
46
+ name: 'hasTimestamps',
47
+ type: 'boolean',
48
+ description: 'Show or hide the timestamp column.',
49
+ default: 'true',
50
+ },
51
+ {
52
+ name: 'label',
53
+ type: 'string',
54
+ description: 'Accessible label for the log region.',
55
+ default: "'Log stream'",
56
+ },
57
+ {
58
+ name: 'renderEntry',
59
+ type: '(entry: LogEntry) => ReactNode',
60
+ description: 'Custom render function for replacing individual row layout.',
61
+ },
62
+ {
63
+ name: 'xstyle',
64
+ type: 'StyleXStyles',
65
+ description: 'StyleX styles for layout customization.',
66
+ },
67
+ ],
17
68
  };
@@ -93,15 +93,28 @@ export const docs = {
93
93
  name: 'hasMarkdownShortcuts',
94
94
  type: 'boolean',
95
95
  description:
96
- 'Enable Markdown shortcut typing (e.g. "# " for a heading). Uses default @lexical/markdown transformers.',
96
+ 'Enable Markdown shortcut typing (e.g. "# " for a heading). Uses the transformers prop (defaults to the standard @lexical/markdown transformers).',
97
97
  default: 'true',
98
98
  },
99
+ {
100
+ name: 'transformers',
101
+ type: 'ReadonlyArray<Transformer>',
102
+ description:
103
+ 'Markdown transformers: the single source of truth for markdown behaviour. Defaults to the standard @lexical/markdown TRANSFORMERS. In Lexical the same array drives all three markdown operations (shortcut typing, markdown->state import, state->markdown export); this prop wires shortcut typing today and is the intended input for the serialization APIs added in later phases. Pass a custom array to support additional node types (e.g. transformers layered in via the nodes extension point) consistently across all three. Shortcut typing is only applied when hasMarkdownShortcuts is true.',
104
+ default: 'TRANSFORMERS',
105
+ },
99
106
  {
100
107
  name: 'hasAutoFocus',
101
108
  type: 'boolean',
102
109
  description: 'Automatically focus the editor on mount.',
103
110
  default: 'false',
104
111
  },
112
+ {
113
+ name: 'maxLength',
114
+ type: 'number',
115
+ description:
116
+ 'Maximum number of characters. When set, a character counter (current/max) is displayed below the editor. Like TextArea, does not enforce the limit natively; the counter shows error styling when the plain-text length exceeds the limit.',
117
+ },
105
118
  {
106
119
  name: 'width',
107
120
  type: 'number | string',
@@ -137,6 +150,11 @@ export const docs = {
137
150
  description:
138
151
  'Register custom node types via the nodes prop on BOTH the editor and the RichTextView so serialized content round-trips.',
139
152
  },
153
+ {
154
+ guidance: true,
155
+ description:
156
+ 'Use a ref (RichTextEditorRef) to imperatively focus(), clear(), read the state via getEditorState(), serialize to Markdown via getMarkdown(), or reach the LexicalEditor via getEditor(). The handle is available after mount. getMarkdown() uses the same transformers prop the editor is configured with. focus() and clear() are no-ops when the editor is read-only or disabled, and clear() resets to a single empty paragraph.',
157
+ },
140
158
  {
141
159
  guidance: false,
142
160
  description: