@eventcatalog/core 2.21.5 → 2.23.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 (30) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-FZHOYLWJ.js → chunk-CLNIEHVG.js} +1 -1
  6. package/dist/{chunk-UYXIOPEC.js → chunk-F55C3RGO.js} +1 -1
  7. package/dist/{chunk-XLSTKMWD.js → chunk-WNRZ5O5C.js} +1 -1
  8. package/dist/constants.cjs +1 -1
  9. package/dist/constants.js +1 -1
  10. package/dist/eventcatalog.cjs +1 -1
  11. package/dist/eventcatalog.config.d.cts +7 -0
  12. package/dist/eventcatalog.config.d.ts +7 -0
  13. package/dist/eventcatalog.js +3 -3
  14. package/eventcatalog/src/components/SideNav/CatalogResourcesSideBar/getCatalogResources.ts +65 -0
  15. package/eventcatalog/src/components/{SideBars → SideNav}/CatalogResourcesSideBar/index.tsx +1 -1
  16. package/eventcatalog/src/components/SideNav/SideNav.astro +31 -0
  17. package/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts +189 -0
  18. package/eventcatalog/src/components/SideNav/TreeView/index.tsx +94 -0
  19. package/eventcatalog/src/components/TreeView/index.tsx +328 -0
  20. package/eventcatalog/src/components/TreeView/styles.module.css +264 -0
  21. package/eventcatalog/src/components/TreeView/useSlots.ts +95 -0
  22. package/eventcatalog/src/content/config.ts +2 -0
  23. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +5 -51
  24. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +55 -0
  25. package/eventcatalog/src/pages/docs/llm/llms-full.txt.ts +31 -0
  26. package/eventcatalog/src/pages/docs/llm/llms.txt.ts +47 -0
  27. package/eventcatalog/src/pages/docs/teams/[id].md.ts +40 -0
  28. package/eventcatalog/src/pages/docs/users/[id].md.ts +36 -0
  29. package/package.json +1 -1
  30. /package/eventcatalog/src/components/{SideBars → SideNav}/CatalogResourcesSideBar/styles.css +0 -0
@@ -0,0 +1,328 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import classes from './styles.module.css';
3
+ import { useSlots } from './useSlots';
4
+ import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
5
+
6
+ // ----------------------------------------------------------------------------
7
+ // Context
8
+
9
+ const RootContext = React.createContext<{
10
+ // We cache the expanded state of tree items so we can preserve the state
11
+ // across remounts. This is necessary because we unmount tree items
12
+ // when their parent is collapsed.
13
+ expandedStateCache: React.RefObject<Map<string, boolean> | null>;
14
+ }>({
15
+ expandedStateCache: { current: new Map() },
16
+ });
17
+
18
+ const ItemContext = React.createContext<{
19
+ level: number;
20
+ isExpanded: boolean;
21
+ }>({
22
+ level: 1,
23
+ isExpanded: false,
24
+ });
25
+
26
+ // ----------------------------------------------------------------------------
27
+ // TreeView
28
+
29
+ export type TreeViewProps = {
30
+ 'aria-label'?: React.AriaAttributes['aria-label'];
31
+ 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
32
+ children: React.ReactNode;
33
+ flat?: boolean;
34
+ truncate?: boolean;
35
+ style?: React.CSSProperties;
36
+ };
37
+
38
+ /* Size of toggle icon in pixels. */
39
+ const TOGGLE_ICON_SIZE = 12;
40
+
41
+ const Root: React.FC<TreeViewProps> = ({
42
+ 'aria-label': ariaLabel,
43
+ 'aria-labelledby': ariaLabelledby,
44
+ children,
45
+ flat,
46
+ truncate = true,
47
+ style,
48
+ }) => {
49
+ const containerRef = React.useRef<HTMLUListElement>(null);
50
+ const mouseDownRef = React.useRef<boolean>(false);
51
+
52
+ const onMouseDown = useCallback(() => {
53
+ mouseDownRef.current = true;
54
+ }, []);
55
+
56
+ useEffect(() => {
57
+ function onMouseUp() {
58
+ mouseDownRef.current = false;
59
+ }
60
+ document.addEventListener('mouseup', onMouseUp);
61
+ return () => {
62
+ document.removeEventListener('mouseup', onMouseUp);
63
+ };
64
+ }, []);
65
+
66
+ const expandedStateCache = React.useRef<Map<string, boolean> | null>(null);
67
+
68
+ if (expandedStateCache.current === null) {
69
+ expandedStateCache.current = new Map();
70
+ }
71
+
72
+ return (
73
+ <RootContext.Provider
74
+ value={{
75
+ expandedStateCache,
76
+ }}
77
+ >
78
+ <ul
79
+ ref={containerRef}
80
+ role="tree"
81
+ aria-label={ariaLabel}
82
+ aria-labelledby={ariaLabelledby}
83
+ data-omit-spacer={flat}
84
+ data-truncate-text={truncate || false}
85
+ onMouseDown={onMouseDown}
86
+ className={classes.TreeViewRootUlStyles}
87
+ style={style}
88
+ >
89
+ {children}
90
+ </ul>
91
+ </RootContext.Provider>
92
+ );
93
+ };
94
+
95
+ Root.displayName = 'TreeView';
96
+
97
+ // ----------------------------------------------------------------------------
98
+ // TreeView.Item
99
+
100
+ export type TreeViewItemProps = {
101
+ id: string;
102
+ children: React.ReactNode;
103
+ current?: boolean;
104
+ defaultExpanded?: boolean;
105
+ onSelect?: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void;
106
+ };
107
+
108
+ const Item = React.forwardRef<HTMLElement, TreeViewItemProps>(
109
+ ({ id: itemId, current: isCurrentItem = false, defaultExpanded, onSelect, children }, ref) => {
110
+ const [slots, rest] = useSlots(children, {
111
+ leadingVisual: LeadingVisual,
112
+ });
113
+ const { expandedStateCache } = React.useContext(RootContext);
114
+
115
+ const [isExpanded, setIsExpanded] = React.useState(
116
+ expandedStateCache.current?.get(itemId) ?? defaultExpanded ?? isCurrentItem
117
+ );
118
+ const { level } = React.useContext(ItemContext);
119
+ const { hasSubTree, subTree, childrenWithoutSubTree } = useSubTree(rest);
120
+ const [isFocused, setIsFocused] = React.useState(false);
121
+
122
+ // Set the expanded state and cache it
123
+ const setIsExpandedWithCache = React.useCallback(
124
+ (newIsExpanded: boolean) => {
125
+ setIsExpanded(newIsExpanded);
126
+ expandedStateCache.current?.set(itemId, newIsExpanded);
127
+ },
128
+ [itemId, setIsExpanded, expandedStateCache]
129
+ );
130
+
131
+ // Expand or collapse the subtree
132
+ const toggle = React.useCallback(
133
+ (event?: React.MouseEvent | React.KeyboardEvent) => {
134
+ setIsExpandedWithCache(!isExpanded);
135
+ event?.stopPropagation();
136
+ },
137
+ [isExpanded, setIsExpandedWithCache]
138
+ );
139
+
140
+ const handleKeyDown = React.useCallback(
141
+ (event: React.KeyboardEvent<HTMLElement>) => {
142
+ switch (event.key) {
143
+ case 'Enter':
144
+ case ' ':
145
+ if (onSelect) {
146
+ onSelect(event);
147
+ } else {
148
+ toggle(event);
149
+ }
150
+ event.stopPropagation();
151
+ break;
152
+ case 'ArrowRight':
153
+ // Ignore if modifier keys are pressed
154
+ if (event.altKey || event.metaKey) return;
155
+ event.preventDefault();
156
+ event.stopPropagation();
157
+ setIsExpandedWithCache(true);
158
+ break;
159
+ case 'ArrowLeft':
160
+ // Ignore if modifier keys are pressed
161
+ if (event.altKey || event.metaKey) return;
162
+ event.preventDefault();
163
+ event.stopPropagation();
164
+ setIsExpandedWithCache(false);
165
+ break;
166
+ }
167
+ },
168
+ [onSelect, setIsExpandedWithCache, toggle]
169
+ );
170
+
171
+ return (
172
+ <ItemContext.Provider
173
+ value={{
174
+ level: level + 1,
175
+ isExpanded,
176
+ }}
177
+ >
178
+ <li
179
+ className={classes.TreeViewItem}
180
+ ref={ref as React.ForwardedRef<HTMLLIElement>}
181
+ tabIndex={0}
182
+ id={itemId}
183
+ role="treeitem"
184
+ aria-level={level}
185
+ aria-expanded={isExpanded}
186
+ aria-current={isCurrentItem ? 'true' : undefined}
187
+ aria-selected={isFocused ? 'true' : 'false'}
188
+ onKeyDown={handleKeyDown}
189
+ onFocus={(event) => {
190
+ // Scroll the first child into view when the item receives focus
191
+ event.currentTarget.firstElementChild?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
192
+
193
+ // Set the focused state
194
+ setIsFocused(true);
195
+
196
+ // Prevent focus event from bubbling up to parent items
197
+ event.stopPropagation();
198
+ }}
199
+ onBlur={() => setIsFocused(false)}
200
+ onClick={(event) => {
201
+ if (onSelect) {
202
+ onSelect(event);
203
+ // if has children open them too
204
+ if (hasSubTree) {
205
+ toggle(event);
206
+ }
207
+ } else {
208
+ toggle(event);
209
+ }
210
+ event.stopPropagation();
211
+ }}
212
+ onAuxClick={(event) => {
213
+ if (onSelect && event.button === 1) {
214
+ onSelect(event);
215
+ }
216
+ event.stopPropagation();
217
+ }}
218
+ >
219
+ <div
220
+ className={classes.TreeViewItemContainer}
221
+ style={{
222
+ // @ts-ignore CSS custom property
223
+ '--level': level,
224
+ }}
225
+ >
226
+ <div style={{ gridArea: 'spacer', display: 'flex' }}>{/* <LevelIndicatorLines level={level} /> */}</div>
227
+
228
+ <div className={classes.TreeViewItemContent}>
229
+ {slots.leadingVisual}
230
+ <span className={classes.TreeViewItemContentText}>{childrenWithoutSubTree}</span>
231
+ </div>
232
+ {hasSubTree ? (
233
+ <div
234
+ className={[classes.TreeViewItemToggle, classes.TreeViewItemToggleHover, classes.TreeViewItemToggleEnd].join(' ')}
235
+ onClick={(event) => {
236
+ if (onSelect) {
237
+ toggle(event);
238
+ }
239
+ }}
240
+ >
241
+ {isExpanded ? <ChevronDownIcon size={TOGGLE_ICON_SIZE} /> : <ChevronRightIcon size={TOGGLE_ICON_SIZE} />}
242
+ </div>
243
+ ) : null}
244
+ </div>
245
+ {subTree}
246
+ </li>
247
+ </ItemContext.Provider>
248
+ );
249
+ }
250
+ );
251
+
252
+ Item.displayName = 'TreeView.Item';
253
+
254
+ // ----------------------------------------------------------------------------
255
+ // TreeView.SubTree
256
+
257
+ export type TreeViewSubTreeProps = {
258
+ children?: React.ReactNode;
259
+ };
260
+
261
+ const SubTree: React.FC<TreeViewSubTreeProps> = ({ children }) => {
262
+ const { isExpanded } = React.useContext(ItemContext);
263
+ const ref = React.useRef<HTMLUListElement>(null);
264
+
265
+ if (!isExpanded) {
266
+ return null;
267
+ }
268
+
269
+ return (
270
+ <ul
271
+ role="group"
272
+ style={{
273
+ listStyle: 'none',
274
+ padding: 0,
275
+ margin: 0,
276
+ }}
277
+ ref={ref}
278
+ >
279
+ {children}
280
+ </ul>
281
+ );
282
+ };
283
+
284
+ SubTree.displayName = 'TreeView.SubTree';
285
+
286
+ function useSubTree(children: React.ReactNode) {
287
+ return React.useMemo(() => {
288
+ const subTree = React.Children.toArray(children).find((child) => React.isValidElement(child) && child.type === SubTree);
289
+
290
+ const childrenWithoutSubTree = React.Children.toArray(children).filter(
291
+ (child) => !(React.isValidElement(child) && child.type === SubTree)
292
+ );
293
+
294
+ return {
295
+ subTree,
296
+ childrenWithoutSubTree,
297
+ hasSubTree: Boolean(subTree),
298
+ };
299
+ }, [children]);
300
+ }
301
+
302
+ // ----------------------------------------------------------------------------
303
+ // TreeView.LeadingVisual
304
+
305
+ export type TreeViewLeadingVisualProps = {
306
+ children: React.ReactNode | ((props: { isExpanded: boolean }) => React.ReactNode);
307
+ };
308
+
309
+ const LeadingVisual: React.FC<TreeViewLeadingVisualProps> = (props) => {
310
+ const { isExpanded } = React.useContext(ItemContext);
311
+ const children = typeof props.children === 'function' ? props.children({ isExpanded }) : props.children;
312
+ return (
313
+ <div className={classes.TreeViewItemVisual} aria-hidden={true}>
314
+ {children}
315
+ </div>
316
+ );
317
+ };
318
+
319
+ LeadingVisual.displayName = 'TreeView.LeadingVisual';
320
+
321
+ // ----------------------------------------------------------------------------
322
+ // Export
323
+
324
+ export const TreeView = Object.assign(Root, {
325
+ Item,
326
+ SubTree,
327
+ LeadingVisual,
328
+ });
@@ -0,0 +1,264 @@
1
+ .TreeViewRootUlStyles {
2
+ padding: 0;
3
+ margin: 0;
4
+ list-style: none;
5
+
6
+ /*
7
+ * WARNING: This is a performance optimization.
8
+ *
9
+ * We define styles for the tree items at the root level of the tree
10
+ * to avoid recomputing the styles for each item when the tree updates.
11
+ * We're sacrificing maintainability for performance because TreeView
12
+ * needs to be performant enough to handle large trees (thousands of items).
13
+ *
14
+ * This is intended to be a temporary solution until we can improve the
15
+ * performance of our styling patterns.
16
+ *
17
+ * Do NOT copy this pattern without understanding the tradeoffs.
18
+ */
19
+ .TreeViewItem {
20
+ outline: none;
21
+
22
+ &:focus-visible > div,
23
+ &.focus-visible > div {
24
+ box-shadow: var(--boxShadow-thick) /* var(--fgColor-accent) */ slategray;
25
+
26
+ @media (forced-colors: active) {
27
+ outline: 2px solid HighlightText;
28
+ outline-offset: -2;
29
+ }
30
+ }
31
+
32
+ &[data-has-leading-action] {
33
+ --has-leading-action: 1;
34
+ }
35
+ }
36
+
37
+ .TreeViewItemContainer {
38
+ --level: 1;
39
+ --toggle-width: 1rem;
40
+ --min-item-height: 2rem;
41
+
42
+ position: relative;
43
+ display: grid;
44
+ width: 100%;
45
+ font-size: var(--text-body-size-medium);
46
+ color: var(--fgColor-default);
47
+ cursor: pointer;
48
+ border-radius: var(--borderRadius-medium);
49
+ grid-template-columns: var(--spacer-width) var(--leading-action-width) 1fr var(--toggle-width);
50
+ grid-template-areas: 'spacer leadingAction content toggle';
51
+
52
+ --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem);
53
+ --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2));
54
+
55
+ &:hover {
56
+ background-color: var(--control-transparent-bgColor-hover);
57
+
58
+ @media (forced-colors: active) {
59
+ outline: 2px solid transparent;
60
+ outline-offset: -2px;
61
+ }
62
+ }
63
+
64
+ @media (pointer: coarse) {
65
+ --toggle-width: 1.5rem;
66
+ --min-item-height: 2.75rem;
67
+ }
68
+
69
+ &:has(.TreeViewItemSkeleton):hover {
70
+ cursor: default;
71
+ background-color: transparent;
72
+
73
+ @media (forced-colors: active) {
74
+ outline: none;
75
+ }
76
+ }
77
+ }
78
+
79
+ &:where([data-omit-spacer='true']) .TreeViewItemContainer {
80
+ grid-template-columns: 0 0 1fr 0;
81
+ }
82
+
83
+ .TreeViewItem[aria-current='true'] > .TreeViewItemContainer {
84
+ background-color: var(--control-transparent-bgColor-selected);
85
+
86
+ /* Current item indicator */
87
+ /* stylelint-disable-next-line selector-max-specificity */
88
+ &::after {
89
+ position: absolute;
90
+ top: calc(50% - var(--base-size-12));
91
+ left: calc(-1 * var(--base-size-8));
92
+ width: 0.25rem;
93
+ height: 1.5rem;
94
+ content: '';
95
+
96
+ /*
97
+ * Use fgColor accent for consistency across all themes. Using the "correct" variable,
98
+ * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode
99
+ */
100
+ /* stylelint-disable-next-line primer/colors */
101
+ background-color: var(--fgColor-accent);
102
+ border-radius: var(--borderRadius-medium);
103
+
104
+ @media (forced-colors: active) {
105
+ background-color: HighlightText;
106
+ }
107
+ }
108
+ }
109
+
110
+ .TreeViewItemToggle {
111
+ display: flex;
112
+ height: 100%;
113
+
114
+ /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap
115
+ across more lines. */
116
+ /* stylelint-disable-next-line primer/spacing */
117
+ padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2);
118
+ color: var(--fgColor-muted);
119
+ grid-area: toggle;
120
+ justify-content: center;
121
+ align-items: flex-start;
122
+ }
123
+
124
+ .TreeViewItemToggleHover:hover {
125
+ background-color: var(--control-transparent-bgColor-hover);
126
+ }
127
+
128
+ .TreeViewItemToggleEnd {
129
+ border-top-left-radius: var(--borderRadius-medium);
130
+ border-bottom-left-radius: var(--borderRadius-medium);
131
+ }
132
+
133
+ .TreeViewItemContent {
134
+ display: flex;
135
+ height: 100%;
136
+ padding: 0 var(--base-size-8);
137
+
138
+ /* The dynamic top and bottom padding to maintain the minimum item height for single line items */
139
+ /* stylelint-disable-next-line primer/spacing */
140
+ padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
141
+ /* stylelint-disable-next-line primer/spacing */
142
+ padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
143
+ line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285));
144
+ grid-area: content;
145
+ gap: var(--stack-gap-condensed);
146
+ }
147
+
148
+ .TreeViewItemContentText {
149
+ flex: 1 1 auto;
150
+ width: 0;
151
+ }
152
+
153
+ &:where([data-truncate-text='true']) .TreeViewItemContentText {
154
+ overflow: hidden;
155
+ text-overflow: ellipsis;
156
+ white-space: nowrap;
157
+ }
158
+
159
+ &:where([data-truncate-text='false']) .TreeViewItemContentText {
160
+ word-break: break-word;
161
+ }
162
+
163
+ .TreeViewItemVisual {
164
+ display: flex;
165
+
166
+ /* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap
167
+ across more lines. */
168
+ height: var(--custom-line-height, 1.3rem);
169
+ color: var(--fgColor-muted);
170
+ align-items: center;
171
+ }
172
+
173
+ .TreeViewItemLeadingAction {
174
+ display: flex;
175
+ color: var(--fgColor-muted);
176
+ grid-area: leadingAction;
177
+
178
+ & > button {
179
+ flex-shrink: 1;
180
+ }
181
+ }
182
+
183
+ .TreeViewItemLevelLine {
184
+ width: 100%;
185
+ height: 100%;
186
+
187
+ /*
188
+ * On devices without hover, the nesting indicator lines
189
+ * appear at all times.
190
+ */
191
+ border-color: var(--borderColor-muted);
192
+ border-right: var(--borderWidth-thin) solid;
193
+ }
194
+
195
+ /*
196
+ * On devices with :hover support, the nesting indicator lines
197
+ * fade in when the user mouses over the entire component,
198
+ * or when there's focus inside the component. This makes
199
+ * sure the component remains simple when not in use.
200
+ */
201
+ @media (hover: hover) {
202
+ .TreeViewItemLevelLine {
203
+ border-color: transparent;
204
+ }
205
+
206
+ &:hover .TreeViewItemLevelLine,
207
+ &:focus-within .TreeViewItemLevelLine {
208
+ border-color: var(--borderColor-muted);
209
+ }
210
+ }
211
+
212
+ .TreeViewDirectoryIcon {
213
+ display: grid;
214
+ color: var(--treeViewItem-leadingVisual-iconColor-rest);
215
+ }
216
+
217
+ .TreeViewVisuallyHidden {
218
+ position: absolute;
219
+ width: 1px;
220
+ height: 1px;
221
+ padding: 0;
222
+ /* stylelint-disable-next-line primer/spacing */
223
+ margin: -1px;
224
+ overflow: hidden;
225
+ clip: rect(0, 0, 0, 0);
226
+ white-space: nowrap;
227
+ border-width: 0;
228
+ }
229
+ }
230
+
231
+ .TreeViewSkeletonItemContainerStyle {
232
+ display: flex;
233
+ align-items: center;
234
+ column-gap: 0.5rem;
235
+ height: 2rem;
236
+
237
+ @media (pointer: coarse) {
238
+ height: 2.75rem;
239
+ }
240
+
241
+ &:nth-of-type(5n + 1) {
242
+ --tree-item-loading-width: 67%;
243
+ }
244
+
245
+ &:nth-of-type(5n + 2) {
246
+ --tree-item-loading-width: 47%;
247
+ }
248
+
249
+ &:nth-of-type(5n + 3) {
250
+ --tree-item-loading-width: 73%;
251
+ }
252
+
253
+ &:nth-of-type(5n + 4) {
254
+ --tree-item-loading-width: 64%;
255
+ }
256
+
257
+ &:nth-of-type(5n + 5) {
258
+ --tree-item-loading-width: 50%;
259
+ }
260
+ }
261
+
262
+ .TreeItemSkeletonTextStyles {
263
+ width: var(--tree-item-loading-width, 67%);
264
+ }
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ // import {warning} from '../utils/warning'
3
+
4
+ // slot config allows 2 options:
5
+ // 1. Component to match, example: { leadingVisual: LeadingVisual }
6
+ type ComponentMatcher = React.ElementType<Props>;
7
+ // 2. Component to match + a test function, example: { blockDescription: [Description, props => props.variant === 'block'] }
8
+ type ComponentAndPropsMatcher = [ComponentMatcher, (props: Props) => boolean];
9
+
10
+ export type SlotConfig = Record<string, ComponentMatcher | ComponentAndPropsMatcher>;
11
+
12
+ // We don't know what the props are yet, we set them later based on slot config
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ type Props = any;
15
+
16
+ type SlotElements<Config extends SlotConfig> = {
17
+ [Property in keyof Config]: SlotValue<Config, Property>;
18
+ };
19
+
20
+ type SlotValue<Config, Property extends keyof Config> = Config[Property] extends React.ElementType // config option 1
21
+ ? React.ReactElement<React.ComponentPropsWithoutRef<Config[Property]>, Config[Property]>
22
+ : Config[Property] extends readonly [
23
+ infer ElementType extends React.ElementType, // config option 2, infer array[0] as component
24
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
+ infer _testFn, // even though we don't use testFn, we need to infer it to support types for slots.*.props
26
+ ]
27
+ ? React.ReactElement<React.ComponentPropsWithoutRef<ElementType>, ElementType>
28
+ : never; // useful for narrowing types, third option is not possible
29
+
30
+ /**
31
+ * Extract components from `children` so we can render them in different places,
32
+ * allowing us to implement components with SSR-compatible slot APIs.
33
+ * Note: We can only extract direct children, not nested ones.
34
+ */
35
+ export function useSlots<Config extends SlotConfig>(
36
+ children: React.ReactNode,
37
+ config: Config
38
+ ): [Partial<SlotElements<Config>>, React.ReactNode[]] {
39
+ // Object mapping slot names to their elements
40
+ const slots: Partial<SlotElements<Config>> = mapValues(config, () => undefined);
41
+
42
+ // Array of elements that are not slots
43
+ const rest: React.ReactNode[] = [];
44
+
45
+ const keys = Object.keys(config) as Array<keyof Config>;
46
+ const values = Object.values(config);
47
+
48
+ // eslint-disable-next-line github/array-foreach
49
+ React.Children.forEach(children, (child) => {
50
+ if (!React.isValidElement(child)) {
51
+ rest.push(child);
52
+ return;
53
+ }
54
+
55
+ const index = values.findIndex((value) => {
56
+ if (Array.isArray(value)) {
57
+ const [component, testFn] = value;
58
+ return child.type === component && testFn(child.props);
59
+ } else {
60
+ return child.type === value;
61
+ }
62
+ });
63
+
64
+ // If the child is not a slot, add it to the `rest` array
65
+ if (index === -1) {
66
+ rest.push(child);
67
+ return;
68
+ }
69
+
70
+ const slotKey = keys[index];
71
+
72
+ // If slot is already filled, ignore duplicates
73
+ if (slots[slotKey]) {
74
+ // warning(true, `Found duplicate "${String(slotKey)}" slot. Only the first will be rendered.`)
75
+ return;
76
+ }
77
+
78
+ // If the child is a slot, add it to the `slots` object
79
+
80
+ slots[slotKey] = child as SlotValue<Config, keyof Config>;
81
+ });
82
+
83
+ return [slots, rest];
84
+ }
85
+
86
+ /** Map the values of an object */
87
+ function mapValues<T extends Record<string, unknown>, V>(obj: T, fn: (value: T[keyof T]) => V) {
88
+ return Object.keys(obj).reduce(
89
+ (result, key: keyof T) => {
90
+ result[key] = fn(obj[key]);
91
+ return result;
92
+ },
93
+ {} as Record<keyof T, V>
94
+ );
95
+ }
@@ -269,6 +269,7 @@ const users = defineCollection({
269
269
  ownedCommands: z.array(reference('commands')).optional(),
270
270
  ownedQueries: z.array(reference('queries')).optional(),
271
271
  associatedTeams: z.array(reference('teams')).optional(),
272
+ pathToFile: z.string().optional(),
272
273
  }),
273
274
  });
274
275
 
@@ -288,6 +289,7 @@ const teams = defineCollection({
288
289
  ownedDomains: z.array(reference('domains')).optional(),
289
290
  ownedServices: z.array(reference('services')).optional(),
290
291
  ownedEvents: z.array(reference('events')).optional(),
292
+ pathToFile: z.string().optional(),
291
293
  }),
292
294
  });
293
295