@spfx-fast/sharepoint-ui 0.4.3 → 0.5.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/QUICKSTART.md +13 -128
- package/README.md +19 -26
- package/components.json +240 -5834
- package/dist/accordion.d.ts +11 -0
- package/dist/accordion.js +4 -0
- package/dist/alert.d.ts +3 -0
- package/dist/alert.js +4 -0
- package/dist/attachment-list.d.ts +15 -0
- package/dist/attachment-list.js +14 -0
- package/dist/avatar-group.d.ts +12 -0
- package/dist/avatar-group.js +7 -0
- package/dist/avatar.d.ts +6 -0
- package/dist/avatar.js +5 -0
- package/dist/badge.d.ts +3 -0
- package/dist/badge.js +4 -0
- package/dist/breadcrumb.d.ts +10 -0
- package/dist/breadcrumb.js +9 -0
- package/dist/button-group.d.ts +6 -0
- package/dist/button-group.js +4 -0
- package/dist/button.d.ts +3 -0
- package/dist/button.js +4 -0
- package/dist/card.d.ts +3 -0
- package/dist/card.js +4 -0
- package/dist/carousel.d.ts +11 -0
- package/dist/carousel.js +6 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.js +4 -0
- package/dist/combobox.d.ts +9 -0
- package/dist/combobox.js +6 -0
- package/dist/command-bar.d.ts +6 -0
- package/dist/command-bar.js +4 -0
- package/dist/copy-button.d.ts +7 -0
- package/dist/copy-button.js +19 -0
- package/dist/data-grid.d.ts +25 -0
- package/dist/data-grid.js +39 -0
- package/dist/date-range-picker.d.ts +12 -0
- package/dist/date-range-picker.js +4 -0
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +21 -0
- package/dist/divider.d.ts +3 -0
- package/dist/divider.js +4 -0
- package/dist/drawer.d.ts +6 -0
- package/dist/drawer.js +5 -0
- package/dist/empty-state.d.ts +7 -0
- package/dist/empty-state.js +5 -0
- package/dist/field.d.ts +15 -0
- package/dist/field.js +11 -0
- package/dist/file-dropzone.d.ts +15 -0
- package/dist/file-dropzone.js +21 -0
- package/dist/filter-bar.d.ts +9 -0
- package/dist/filter-bar.js +9 -0
- package/dist/image.d.ts +5 -0
- package/dist/image.js +4 -0
- package/dist/index.d.ts +68 -78
- package/dist/index.js +60 -68
- package/dist/input.d.ts +3 -0
- package/dist/input.js +4 -0
- package/dist/label.d.ts +3 -0
- package/dist/label.js +4 -0
- package/dist/link.d.ts +3 -0
- package/dist/link.js +9 -0
- package/dist/list-view.d.ts +29 -0
- package/dist/list-view.js +14 -0
- package/dist/list.d.ts +6 -0
- package/dist/list.js +5 -0
- package/dist/lookup-picker.d.ts +12 -0
- package/dist/lookup-picker.js +4 -0
- package/dist/menu.d.ts +6 -0
- package/dist/menu.js +4 -0
- package/dist/nav.d.ts +12 -0
- package/dist/nav.js +4 -0
- package/dist/pagination.d.ts +11 -0
- package/dist/pagination.js +5 -0
- package/dist/person-picker.d.ts +13 -0
- package/dist/person-picker.js +4 -0
- package/dist/persona.d.ts +7 -0
- package/dist/persona.js +5 -0
- package/dist/popover.d.ts +6 -0
- package/dist/popover.js +4 -0
- package/dist/progress-bar.d.ts +5 -0
- package/dist/progress-bar.js +5 -0
- package/dist/radio-group.d.ts +6 -0
- package/dist/radio-group.js +4 -0
- package/dist/radio.d.ts +3 -0
- package/dist/radio.js +4 -0
- package/dist/rating.d.ts +8 -0
- package/dist/rating.js +5 -0
- package/dist/react.d.ts +1 -4715
- package/dist/react.js +1 -7097
- package/dist/select.d.ts +3 -0
- package/dist/select.js +4 -0
- package/dist/sharepoint-field.d.ts +18 -0
- package/dist/sharepoint-field.js +54 -0
- package/dist/skeleton.d.ts +6 -0
- package/dist/skeleton.js +9 -0
- package/dist/spinner.d.ts +5 -0
- package/dist/spinner.js +4 -0
- package/dist/swatch-picker.d.ts +17 -0
- package/dist/swatch-picker.js +11 -0
- package/dist/switch.d.ts +3 -0
- package/dist/switch.js +4 -0
- package/dist/table.d.ts +6 -0
- package/dist/table.js +4 -0
- package/dist/tabs.d.ts +14 -0
- package/dist/tabs.js +30 -0
- package/dist/tag-group.d.ts +6 -0
- package/dist/tag-group.js +4 -0
- package/dist/tag-picker.d.ts +12 -0
- package/dist/tag-picker.js +4 -0
- package/dist/tag.d.ts +7 -0
- package/dist/tag.js +5 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.js +4 -0
- package/dist/textarea.d.ts +3 -0
- package/dist/textarea.js +4 -0
- package/dist/theme.d.ts +15 -175
- package/dist/theme.js +36 -239
- package/dist/toast.d.ts +9 -0
- package/dist/toast.js +12 -0
- package/dist/tree.d.ts +13 -0
- package/dist/tree.js +9 -0
- package/dist/view-card.d.ts +8 -0
- package/dist/view-card.js +4 -0
- package/dist/view-switcher.d.ts +13 -0
- package/dist/view-switcher.js +7 -0
- package/dist/virtual-list.d.ts +11 -0
- package/dist/virtual-list.js +25 -0
- package/measurements.json +365 -0
- package/package.json +14 -46
- package/src/theme.css +125 -0
- package/AGENTS.md +0 -231
- package/dist/appearance.d.ts +0 -6
- package/dist/appearance.js +0 -217
- package/dist/elements/accordion.d.ts +0 -9
- package/dist/elements/accordion.js +0 -338
- package/dist/elements/alert.d.ts +0 -2
- package/dist/elements/alert.js +0 -86
- package/dist/elements/avatar-group-primitives.d.ts +0 -3
- package/dist/elements/avatar-group-primitives.js +0 -189
- package/dist/elements/avatar-group.d.ts +0 -2
- package/dist/elements/avatar-group.js +0 -110
- package/dist/elements/avatar.d.ts +0 -4
- package/dist/elements/avatar.js +0 -251
- package/dist/elements/badge.d.ts +0 -8
- package/dist/elements/badge.js +0 -182
- package/dist/elements/breadcrumb.d.ts +0 -4
- package/dist/elements/breadcrumb.js +0 -233
- package/dist/elements/button-group.d.ts +0 -1
- package/dist/elements/button-group.js +0 -20
- package/dist/elements/button-variants.d.ts +0 -6
- package/dist/elements/button-variants.js +0 -339
- package/dist/elements/button.d.ts +0 -5
- package/dist/elements/button.js +0 -233
- package/dist/elements/card-primitives.d.ts +0 -3
- package/dist/elements/card-primitives.js +0 -83
- package/dist/elements/card.d.ts +0 -5
- package/dist/elements/card.js +0 -213
- package/dist/elements/carousel.d.ts +0 -92
- package/dist/elements/carousel.js +0 -1115
- package/dist/elements/color-picker.d.ts +0 -24
- package/dist/elements/color-picker.js +0 -1004
- package/dist/elements/combobox.d.ts +0 -1
- package/dist/elements/combobox.js +0 -1012
- package/dist/elements/data-grid-model.d.ts +0 -120
- package/dist/elements/data-grid-model.js +0 -294
- package/dist/elements/data-grid.d.ts +0 -30
- package/dist/elements/data-grid.js +0 -1576
- package/dist/elements/date-picker.d.ts +0 -1
- package/dist/elements/date-picker.js +0 -486
- package/dist/elements/dialog.d.ts +0 -23
- package/dist/elements/dialog.js +0 -409
- package/dist/elements/divider.d.ts +0 -2
- package/dist/elements/divider.js +0 -66
- package/dist/elements/drawer.d.ts +0 -3
- package/dist/elements/drawer.js +0 -308
- package/dist/elements/field-composition.d.ts +0 -26
- package/dist/elements/field-composition.js +0 -133
- package/dist/elements/field.d.ts +0 -1
- package/dist/elements/field.js +0 -67
- package/dist/elements/file-upload.d.ts +0 -6
- package/dist/elements/file-upload.js +0 -211
- package/dist/elements/floating-position.d.ts +0 -36
- package/dist/elements/floating-position.js +0 -92
- package/dist/elements/image.d.ts +0 -28
- package/dist/elements/image.js +0 -38
- package/dist/elements/info.d.ts +0 -9
- package/dist/elements/info.js +0 -102
- package/dist/elements/input.d.ts +0 -5
- package/dist/elements/input.js +0 -255
- package/dist/elements/label.d.ts +0 -15
- package/dist/elements/label.js +0 -56
- package/dist/elements/link.d.ts +0 -1
- package/dist/elements/link.js +0 -92
- package/dist/elements/list.d.ts +0 -19
- package/dist/elements/list.js +0 -297
- package/dist/elements/menu.d.ts +0 -33
- package/dist/elements/menu.js +0 -1103
- package/dist/elements/message-bar.d.ts +0 -10
- package/dist/elements/message-bar.js +0 -327
- package/dist/elements/nav.d.ts +0 -32
- package/dist/elements/nav.js +0 -794
- package/dist/elements/overflow.d.ts +0 -10
- package/dist/elements/overflow.js +0 -218
- package/dist/elements/persona.d.ts +0 -3
- package/dist/elements/persona.js +0 -125
- package/dist/elements/popover.d.ts +0 -77
- package/dist/elements/popover.js +0 -840
- package/dist/elements/progress-bar.d.ts +0 -4
- package/dist/elements/progress-bar.js +0 -82
- package/dist/elements/radio-group.d.ts +0 -1
- package/dist/elements/radio-group.js +0 -303
- package/dist/elements/rating-display.d.ts +0 -2
- package/dist/elements/rating-display.js +0 -125
- package/dist/elements/rating.d.ts +0 -2
- package/dist/elements/rating.js +0 -206
- package/dist/elements/rich-text-editor.d.ts +0 -32
- package/dist/elements/rich-text-editor.js +0 -1367
- package/dist/elements/search.d.ts +0 -3
- package/dist/elements/search.js +0 -200
- package/dist/elements/select.d.ts +0 -6
- package/dist/elements/select.js +0 -291
- package/dist/elements/shared.d.ts +0 -22
- package/dist/elements/shared.js +0 -409
- package/dist/elements/sharepoint-field-basic-controls.d.ts +0 -1
- package/dist/elements/sharepoint-field-basic-controls.js +0 -14
- package/dist/elements/sharepoint-fields.d.ts +0 -29
- package/dist/elements/sharepoint-fields.js +0 -460
- package/dist/elements/skeleton.d.ts +0 -6
- package/dist/elements/skeleton.js +0 -91
- package/dist/elements/slider.d.ts +0 -1
- package/dist/elements/slider.js +0 -248
- package/dist/elements/spin-button.d.ts +0 -7
- package/dist/elements/spin-button.js +0 -499
- package/dist/elements/spinner.d.ts +0 -4
- package/dist/elements/spinner.js +0 -124
- package/dist/elements/swatch-picker.d.ts +0 -5
- package/dist/elements/swatch-picker.js +0 -363
- package/dist/elements/table.d.ts +0 -21
- package/dist/elements/table.js +0 -261
- package/dist/elements/tabs.d.ts +0 -1
- package/dist/elements/tabs.js +0 -276
- package/dist/elements/tag-picker.d.ts +0 -29
- package/dist/elements/tag-picker.js +0 -715
- package/dist/elements/tags.d.ts +0 -21
- package/dist/elements/tags.js +0 -421
- package/dist/elements/teaching-popover.d.ts +0 -32
- package/dist/elements/teaching-popover.js +0 -412
- package/dist/elements/text.d.ts +0 -9
- package/dist/elements/text.js +0 -75
- package/dist/elements/textarea.d.ts +0 -1
- package/dist/elements/textarea.js +0 -182
- package/dist/elements/toast.d.ts +0 -25
- package/dist/elements/toast.js +0 -337
- package/dist/elements/toaster.d.ts +0 -6
- package/dist/elements/toaster.js +0 -357
- package/dist/elements/toggle.d.ts +0 -2
- package/dist/elements/toggle.js +0 -246
- package/dist/elements/toolbar.d.ts +0 -12
- package/dist/elements/toolbar.js +0 -381
- package/dist/elements/tooltip.d.ts +0 -76
- package/dist/elements/tooltip.js +0 -527
- package/dist/elements/tree-model.d.ts +0 -19
- package/dist/elements/tree-model.js +0 -34
- package/dist/elements/tree.d.ts +0 -5
- package/dist/elements/tree.js +0 -273
- package/dist/elements/view-card.d.ts +0 -2
- package/dist/elements/view-card.js +0 -78
- package/dist/elements/virtual-list.d.ts +0 -41
- package/dist/elements/virtual-list.js +0 -369
- package/dist/elements.d.ts +0 -73
- package/dist/elements.js +0 -210
- package/dist/sharepoint-fields.d.ts +0 -135
- package/dist/sharepoint-fields.js +0 -402
- package/dist/virtualizer-hooks.d.ts +0 -80
- package/dist/virtualizer-hooks.js +0 -253
- package/scripts/generate-token-index.mjs +0 -43
- package/scripts/sync-component-catalog-signatures.mjs +0 -55
- package/scripts/validate-component-catalog.mjs +0 -44
- package/skills/sharepoint-ui/SKILL.md +0 -12
- package/skills/sharepoint-ui/references/choosing-components.md +0 -20
- package/skills/sharepoint-ui/references/sharepoint-patterns.md +0 -17
- package/tokens.json +0 -3777
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
function viewportLength(element, direction) {
|
|
3
|
-
if (element === element.ownerDocument.body)
|
|
4
|
-
return direction === "vertical" ? element.ownerDocument.defaultView?.innerHeight ?? 0 : element.ownerDocument.defaultView?.innerWidth ?? 0;
|
|
5
|
-
const rect = element.getBoundingClientRect();
|
|
6
|
-
return direction === "vertical" ? rect.height : rect.width;
|
|
7
|
-
}
|
|
8
|
-
/** Measure a fixed-size viewport and calculate a bounded item window. */
|
|
9
|
-
export function useStaticVirtualizerMeasure(props) {
|
|
10
|
-
const { defaultItemSize, direction = "vertical", bufferItems: configuredBufferItems, bufferSize: configuredBufferSize } = props;
|
|
11
|
-
const [measurement, setMeasurement] = React.useState({ virtualizerLength: 0, bufferItems: 0, bufferSize: 0 });
|
|
12
|
-
const containerSizeRef = React.useRef(0);
|
|
13
|
-
const elementRef = React.useRef(null);
|
|
14
|
-
const update = React.useCallback((element = elementRef.current) => {
|
|
15
|
-
if (!element)
|
|
16
|
-
return;
|
|
17
|
-
const size = Math.max(0, viewportLength(element, direction));
|
|
18
|
-
containerSizeRef.current = size;
|
|
19
|
-
const visible = Math.ceil(size / Math.max(1, defaultItemSize) + 1);
|
|
20
|
-
const next = { virtualizerLength: visible + (configuredBufferItems ?? Math.max(Math.ceil(visible / 4), 1)) * 2, bufferItems: configuredBufferItems ?? Math.max(Math.ceil(visible / 4), 1), bufferSize: configuredBufferSize ?? Math.max(defaultItemSize / 2, 1) };
|
|
21
|
-
setMeasurement(previous => previous.virtualizerLength === next.virtualizerLength && previous.bufferItems === next.bufferItems && previous.bufferSize === next.bufferSize ? previous : next);
|
|
22
|
-
}, [direction, defaultItemSize, configuredBufferItems, configuredBufferSize]);
|
|
23
|
-
const observerRef = useResizeObserverRef_unstable(() => update());
|
|
24
|
-
const scrollRef = React.useCallback((element) => {
|
|
25
|
-
elementRef.current = element;
|
|
26
|
-
observerRef(element);
|
|
27
|
-
if (element)
|
|
28
|
-
update(element);
|
|
29
|
-
}, [observerRef, update]);
|
|
30
|
-
React.useEffect(() => {
|
|
31
|
-
const element = elementRef.current;
|
|
32
|
-
const win = element?.ownerDocument.defaultView;
|
|
33
|
-
if (!element || element !== element.ownerDocument.body || !win)
|
|
34
|
-
return;
|
|
35
|
-
const onResize = () => update();
|
|
36
|
-
win.addEventListener("resize", onResize, { passive: true });
|
|
37
|
-
return () => win.removeEventListener("resize", onResize);
|
|
38
|
-
}, [update]);
|
|
39
|
-
return { ...measurement, scrollRef, containerSizeRef };
|
|
40
|
-
}
|
|
41
|
-
/** Measure only enough variable-size rows to cover the current scroll viewport. */
|
|
42
|
-
export function useDynamicVirtualizerMeasure(props) {
|
|
43
|
-
const { defaultItemSize, direction = "vertical", numItems, getItemSize, bufferItems: configuredBufferItems, bufferSize: configuredBufferSize, virtualizerContext } = props;
|
|
44
|
-
const [measurement, setMeasurement] = React.useState({ virtualizerLength: 0, bufferItems: 0, bufferSize: 0 });
|
|
45
|
-
const containerSizeRef = React.useRef(0);
|
|
46
|
-
const elementRef = React.useRef(null);
|
|
47
|
-
const positionRef = React.useRef(0);
|
|
48
|
-
const update = React.useCallback((element = elementRef.current) => {
|
|
49
|
-
if (!element)
|
|
50
|
-
return;
|
|
51
|
-
containerSizeRef.current = Math.max(0, viewportLength(element, direction));
|
|
52
|
-
const bufferSize = configuredBufferSize ?? Math.max(defaultItemSize / 2, 1);
|
|
53
|
-
const progressiveSizes = virtualizerContext.childProgressiveSizes.current;
|
|
54
|
-
let visible = 0;
|
|
55
|
-
let covered = 0;
|
|
56
|
-
let start = Math.max(0, Math.min(Math.floor(virtualizerContext.contextIndex), Math.max(0, numItems - 1)));
|
|
57
|
-
if (progressiveSizes.length >= numItems) {
|
|
58
|
-
let low = 0;
|
|
59
|
-
let high = numItems;
|
|
60
|
-
while (low < high) {
|
|
61
|
-
const middle = (low + high) >>> 1;
|
|
62
|
-
if (progressiveSizes[middle] <= positionRef.current)
|
|
63
|
-
low = middle + 1;
|
|
64
|
-
else
|
|
65
|
-
high = middle;
|
|
66
|
-
}
|
|
67
|
-
start = Math.min(Math.max(0, numItems - 1), low);
|
|
68
|
-
}
|
|
69
|
-
while (start + visible < numItems && covered < containerSizeRef.current + bufferSize * 2) {
|
|
70
|
-
const estimated = getItemSize(start + visible);
|
|
71
|
-
const size = Number.isFinite(estimated) && estimated > 0 ? estimated : defaultItemSize;
|
|
72
|
-
const end = progressiveSizes[start + visible];
|
|
73
|
-
const beginning = start + visible === 0 ? 0 : progressiveSizes[start + visible - 1];
|
|
74
|
-
const rowStart = Number.isFinite(end) ? beginning : (start + visible) * defaultItemSize;
|
|
75
|
-
const rowEnd = Number.isFinite(end) ? end : rowStart + size;
|
|
76
|
-
if (rowEnd > positionRef.current) {
|
|
77
|
-
covered += Math.max(0, Math.min(rowEnd, positionRef.current + containerSizeRef.current + bufferSize * 2) - Math.max(rowStart, positionRef.current));
|
|
78
|
-
visible++;
|
|
79
|
-
}
|
|
80
|
-
else
|
|
81
|
-
visible++;
|
|
82
|
-
}
|
|
83
|
-
const nextBufferItems = configuredBufferItems ?? Math.max(Math.ceil(visible / 3), 1);
|
|
84
|
-
const next = { virtualizerLength: Math.min(numItems, visible + nextBufferItems * 2), bufferItems: nextBufferItems, bufferSize };
|
|
85
|
-
if (virtualizerContext.contextIndex !== start)
|
|
86
|
-
virtualizerContext.setContextIndex(start);
|
|
87
|
-
setMeasurement(previous => previous.virtualizerLength === next.virtualizerLength && previous.bufferItems === next.bufferItems && previous.bufferSize === next.bufferSize ? previous : next);
|
|
88
|
-
}, [direction, defaultItemSize, numItems, getItemSize, configuredBufferItems, configuredBufferSize, virtualizerContext.contextIndex, virtualizerContext.childProgressiveSizes]);
|
|
89
|
-
const observerRef = useResizeObserverRef_unstable(() => update());
|
|
90
|
-
const scrollRef = React.useCallback((element) => { elementRef.current = element; observerRef(element); if (element)
|
|
91
|
-
update(element); }, [observerRef, update]);
|
|
92
|
-
const updateScrollPosition = React.useCallback((position) => { positionRef.current = Number.isFinite(position) ? Math.max(0, position) : 0; update(); }, [update]);
|
|
93
|
-
React.useLayoutEffect(() => { update(); }, [update]);
|
|
94
|
-
return { ...measurement, scrollRef, containerSizeRef, updateScrollPosition };
|
|
95
|
-
}
|
|
96
|
-
/** Track measured widths/heights by item index for legacy dynamic virtualizer composition. */
|
|
97
|
-
export function useMeasureList(currentIndex, _refLength, totalLength, defaultItemSize) {
|
|
98
|
-
const widthArray = React.useRef(Array(Math.max(0, totalLength)).fill(defaultItemSize));
|
|
99
|
-
const heightArray = React.useRef(Array(Math.max(0, totalLength)).fill(defaultItemSize));
|
|
100
|
-
const refArray = React.useRef([]);
|
|
101
|
-
const [sizeUpdateCount, setSizeUpdateCount] = React.useState(0);
|
|
102
|
-
const observer = React.useRef(null);
|
|
103
|
-
const observerDocument = React.useRef(null);
|
|
104
|
-
const indexByElement = React.useRef(new WeakMap());
|
|
105
|
-
React.useEffect(() => {
|
|
106
|
-
const previousLength = widthArray.current.length;
|
|
107
|
-
widthArray.current.length = Math.max(0, totalLength);
|
|
108
|
-
heightArray.current.length = Math.max(0, totalLength);
|
|
109
|
-
for (let index = previousLength; index < widthArray.current.length; index++)
|
|
110
|
-
widthArray.current[index] = heightArray.current[index] = defaultItemSize;
|
|
111
|
-
refArray.current.length = Math.max(0, totalLength);
|
|
112
|
-
}, [defaultItemSize, totalLength]);
|
|
113
|
-
React.useEffect(() => () => observer.current?.disconnect(), []);
|
|
114
|
-
const measure = React.useCallback((element, index) => {
|
|
115
|
-
const rect = element.getBoundingClientRect();
|
|
116
|
-
const width = rect.width || defaultItemSize;
|
|
117
|
-
const height = rect.height || defaultItemSize;
|
|
118
|
-
if (widthArray.current[index] !== width || heightArray.current[index] !== height) {
|
|
119
|
-
widthArray.current[index] = width;
|
|
120
|
-
heightArray.current[index] = height;
|
|
121
|
-
setSizeUpdateCount(count => count + 1);
|
|
122
|
-
}
|
|
123
|
-
}, [defaultItemSize]);
|
|
124
|
-
const createIndexedRef = React.useCallback((index) => (element) => {
|
|
125
|
-
const absoluteIndex = Math.max(0, Math.floor(currentIndex + index));
|
|
126
|
-
const prior = refArray.current[absoluteIndex];
|
|
127
|
-
if (prior && prior !== element)
|
|
128
|
-
observer.current?.unobserve(prior);
|
|
129
|
-
refArray.current[absoluteIndex] = element;
|
|
130
|
-
if (!element)
|
|
131
|
-
return;
|
|
132
|
-
indexByElement.current.set(element, absoluteIndex);
|
|
133
|
-
element.handleResize = () => measure(element, absoluteIndex);
|
|
134
|
-
const ResizeObserverCtor = element.ownerDocument.defaultView?.ResizeObserver;
|
|
135
|
-
if (ResizeObserverCtor && (!observer.current || observerDocument.current !== element.ownerDocument)) {
|
|
136
|
-
observer.current?.disconnect();
|
|
137
|
-
const nextObserver = createResizeObserverFromDocument(element.ownerDocument, entries => entries.forEach(entry => {
|
|
138
|
-
const row = entry.target;
|
|
139
|
-
const rowIndex = indexByElement.current.get(row);
|
|
140
|
-
if (rowIndex !== undefined)
|
|
141
|
-
measure(row, rowIndex);
|
|
142
|
-
}));
|
|
143
|
-
observer.current = nextObserver;
|
|
144
|
-
observerDocument.current = element.ownerDocument;
|
|
145
|
-
}
|
|
146
|
-
observer.current?.observe(element);
|
|
147
|
-
measure(element, absoluteIndex);
|
|
148
|
-
}, [currentIndex, measure]);
|
|
149
|
-
return { widthArray, heightArray, createIndexedRef, refArray, sizeUpdateCount };
|
|
150
|
-
}
|
|
151
|
-
/** Create an observer from the document's own window, including when an SPFx part is rendered in an iframe. */
|
|
152
|
-
export function createResizeObserverFromDocument(targetDocument, callback) {
|
|
153
|
-
const Observer = targetDocument?.defaultView?.ResizeObserver;
|
|
154
|
-
return Observer ? new Observer(callback) : null;
|
|
155
|
-
}
|
|
156
|
-
/** Mirror a four-sided LTR observer margin for an RTL root. */
|
|
157
|
-
export function getRTLRootMargin(ltrRootMargin, target, win) {
|
|
158
|
-
const element = target?.nodeType === 9 ? target.documentElement : target;
|
|
159
|
-
if (!element || !win || win.getComputedStyle(element).direction !== "rtl")
|
|
160
|
-
return ltrRootMargin;
|
|
161
|
-
const margins = ltrRootMargin.trim().split(/\s+/);
|
|
162
|
-
return margins.length === 4 ? `${margins[0]} ${margins[3]} ${margins[2]} ${margins[1]}` : ltrRootMargin;
|
|
163
|
-
}
|
|
164
|
-
/** Observe a changing element list with native IntersectionObserver and direction-aware margins. */
|
|
165
|
-
export function useIntersectionObserver(callback, options) {
|
|
166
|
-
const [elements, setElements] = React.useState();
|
|
167
|
-
const [init, setInit] = React.useState(options);
|
|
168
|
-
const observer = React.useRef(undefined);
|
|
169
|
-
const callbackRef = React.useRef(callback);
|
|
170
|
-
const [directionRevision, setDirectionRevision] = React.useState(0);
|
|
171
|
-
callbackRef.current = callback;
|
|
172
|
-
const setObserverInit = React.useCallback((next) => setInit(next), []);
|
|
173
|
-
React.useEffect(() => {
|
|
174
|
-
const root = init?.root;
|
|
175
|
-
const doc = root?.ownerDocument ?? (typeof document === "undefined" ? undefined : document);
|
|
176
|
-
const MutationObserverCtor = doc?.defaultView?.MutationObserver;
|
|
177
|
-
if (!MutationObserverCtor || !root)
|
|
178
|
-
return;
|
|
179
|
-
const mutationObserver = new MutationObserverCtor(records => {
|
|
180
|
-
if (records.some(record => record.attributeName === "dir"))
|
|
181
|
-
setDirectionRevision(revision => revision + 1);
|
|
182
|
-
});
|
|
183
|
-
mutationObserver.observe(doc.documentElement, { attributes: true, subtree: true, attributeFilter: ["dir"] });
|
|
184
|
-
return () => mutationObserver.disconnect();
|
|
185
|
-
}, [init?.root]);
|
|
186
|
-
React.useEffect(() => {
|
|
187
|
-
const root = init?.root;
|
|
188
|
-
const doc = (root?.nodeType === 1 ? root.ownerDocument : typeof document === "undefined" ? undefined : document);
|
|
189
|
-
const win = doc?.defaultView;
|
|
190
|
-
if (!win?.IntersectionObserver)
|
|
191
|
-
return;
|
|
192
|
-
const rootMargin = getRTLRootMargin(init?.rootMargin ?? "0px", init?.root, win);
|
|
193
|
-
const instance = new win.IntersectionObserver((entries, current) => callbackRef.current(entries, current), { ...init, rootMargin });
|
|
194
|
-
observer.current = instance;
|
|
195
|
-
elements?.forEach(element => instance.observe(element));
|
|
196
|
-
return () => {
|
|
197
|
-
instance.disconnect();
|
|
198
|
-
if (observer.current === instance)
|
|
199
|
-
observer.current = undefined;
|
|
200
|
-
};
|
|
201
|
-
}, [elements, init, directionRevision]);
|
|
202
|
-
return { setObserverList: setElements, setObserverInit, observer };
|
|
203
|
-
}
|
|
204
|
-
/** Attach a resize observer callback ref; it is safe when ResizeObserver is unavailable. */
|
|
205
|
-
export function useResizeObserverRef_unstable(resizeCallback) {
|
|
206
|
-
const callbackRef = React.useRef(resizeCallback);
|
|
207
|
-
callbackRef.current = resizeCallback;
|
|
208
|
-
const observed = React.useRef(null);
|
|
209
|
-
React.useEffect(() => () => { observed.current?.observer.disconnect(); observed.current = null; }, []);
|
|
210
|
-
return React.useCallback(instance => {
|
|
211
|
-
const current = observed.current;
|
|
212
|
-
if (current?.element === instance)
|
|
213
|
-
return;
|
|
214
|
-
current?.observer.disconnect();
|
|
215
|
-
observed.current = null;
|
|
216
|
-
const Observer = instance?.ownerDocument.defaultView?.ResizeObserver;
|
|
217
|
-
if (!instance || !Observer)
|
|
218
|
-
return;
|
|
219
|
-
const target = { current: instance };
|
|
220
|
-
const observer = new Observer((entries, currentObserver) => callbackRef.current(entries, currentObserver, target));
|
|
221
|
-
observed.current = { element: instance, observer };
|
|
222
|
-
observer.observe(instance);
|
|
223
|
-
}, []);
|
|
224
|
-
}
|
|
225
|
-
/** Scroll an element to a fixed-size virtual item. */
|
|
226
|
-
export function scrollToItemStatic({ index, itemSize, totalItems, scrollViewRef, axis = "vertical", reversed = false, behavior = "auto" }) {
|
|
227
|
-
const view = scrollViewRef.current;
|
|
228
|
-
if (!view || !Number.isFinite(index) || !Number.isFinite(itemSize) || itemSize <= 0 || totalItems <= 0)
|
|
229
|
-
return;
|
|
230
|
-
const safeIndex = Math.max(0, Math.min(totalItems - 1, Math.floor(index)));
|
|
231
|
-
const offset = reversed ? Math.max(0, totalItems * itemSize - itemSize * (safeIndex + 1)) : safeIndex * itemSize;
|
|
232
|
-
if (axis === "horizontal")
|
|
233
|
-
view.scrollTo({ left: offset, behavior });
|
|
234
|
-
else
|
|
235
|
-
view.scrollTo({ top: offset, behavior });
|
|
236
|
-
}
|
|
237
|
-
/** Scroll an element to a measured item; summing the prefix is O(index), so keep the measured list modest. */
|
|
238
|
-
export function scrollToItemDynamic({ index, itemSizes, totalSize, scrollViewRef, axis = "vertical", reversed = false, behavior = "auto" }) {
|
|
239
|
-
const view = scrollViewRef.current;
|
|
240
|
-
const sizes = itemSizes.current;
|
|
241
|
-
if (!view || !sizes || !Number.isFinite(index) || !sizes.length)
|
|
242
|
-
return;
|
|
243
|
-
const safeIndex = Math.max(0, Math.min(sizes.length - 1, Math.floor(index)));
|
|
244
|
-
let offset = 0;
|
|
245
|
-
for (let item = 0; item < safeIndex; item++)
|
|
246
|
-
offset += Number.isFinite(sizes[item]) ? Math.max(0, sizes[item]) : 0;
|
|
247
|
-
if (reversed)
|
|
248
|
-
offset = Math.max(0, totalSize - offset - (Number.isFinite(sizes[safeIndex]) ? Math.max(0, sizes[safeIndex]) : 0));
|
|
249
|
-
if (axis === "horizontal")
|
|
250
|
-
view.scrollTo({ left: offset, behavior });
|
|
251
|
-
else
|
|
252
|
-
view.scrollTo({ top: offset, behavior });
|
|
253
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
-
const sourceDir = resolve(packageDir, "src");
|
|
7
|
-
const outputPath = resolve(packageDir, "tokens.json");
|
|
8
|
-
const checkOnly = process.argv.includes("--check");
|
|
9
|
-
const components = new Map();
|
|
10
|
-
const declarations = new Map();
|
|
11
|
-
const addTokens = (map, source, text) => {
|
|
12
|
-
for (const token of text.match(/--spui-[\w-]+/g) ?? []) {
|
|
13
|
-
if (token === "--spui-palette-") continue;
|
|
14
|
-
const sources = map.get(token) ?? new Set();
|
|
15
|
-
sources.add(source);
|
|
16
|
-
map.set(token, sources);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
for (const file of (await readdir(resolve(sourceDir, "elements"))).filter(name => name.endsWith(".ts")).sort()) {
|
|
21
|
-
addTokens(components, file.slice(0, -3), await readFile(resolve(sourceDir, "elements", file), "utf8"));
|
|
22
|
-
}
|
|
23
|
-
for (const file of ["appearance.ts", "theme.ts"]) {
|
|
24
|
-
addTokens(declarations, file.slice(0, -3), await readFile(resolve(sourceDir, file), "utf8"));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const tokens = [...new Set([...components.keys(), ...declarations.keys()])].sort().map(name => ({
|
|
28
|
-
name,
|
|
29
|
-
components: [...(components.get(name) ?? [])].sort(),
|
|
30
|
-
declaredBy: [...(declarations.get(name) ?? [])].sort()
|
|
31
|
-
}));
|
|
32
|
-
const dynamicPrefixes = [{ prefix: "--spui-palette-", declaredBy: ["theme"] }];
|
|
33
|
-
const content = `${JSON.stringify({ generatedFrom: "src/elements/*.ts, src/appearance.ts, src/theme.ts", dynamicPrefixes, tokens }, null, 2)}\n`;
|
|
34
|
-
if (checkOnly) {
|
|
35
|
-
const current = await readFile(outputPath, "utf8").catch(() => "");
|
|
36
|
-
if (current !== content) {
|
|
37
|
-
console.error("Token index is stale. Run npm run tokens:generate --workspace @spfx-fast/sharepoint-ui.");
|
|
38
|
-
process.exitCode = 1;
|
|
39
|
-
} else console.log(`Token index is current (${tokens.length} tokens).`);
|
|
40
|
-
} else {
|
|
41
|
-
await writeFile(outputPath, content);
|
|
42
|
-
console.log(`Wrote ${tokens.length} component token entries to ${outputPath}.`);
|
|
43
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import ts from "typescript";
|
|
5
|
-
|
|
6
|
-
const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
-
const catalogPath = resolve(pkgDir, "components.json");
|
|
8
|
-
const adapterPath = resolve(pkgDir, "src/react.tsx");
|
|
9
|
-
const catalog = JSON.parse(await readFile(catalogPath, "utf8"));
|
|
10
|
-
const adapterText = await readFile(adapterPath, "utf8");
|
|
11
|
-
const source = ts.createSourceFile(adapterPath, adapterText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
12
|
-
const components = new Map();
|
|
13
|
-
|
|
14
|
-
function collect(node) {
|
|
15
|
-
if (ts.isFunctionDeclaration(node) && node.name && node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
16
|
-
components.set(node.name.text, node);
|
|
17
|
-
}
|
|
18
|
-
if (ts.isVariableStatement(node) && node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
19
|
-
for (const declaration of node.declarationList.declarations) {
|
|
20
|
-
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
21
|
-
function findNamedFunction(candidate) {
|
|
22
|
-
if (ts.isFunctionExpression(candidate) && candidate.name?.text === declaration.name.text) return candidate;
|
|
23
|
-
let found;
|
|
24
|
-
ts.forEachChild(candidate, child => { found ??= findNamedFunction(child); });
|
|
25
|
-
return found;
|
|
26
|
-
}
|
|
27
|
-
const component = findNamedFunction(declaration.initializer);
|
|
28
|
-
if (component) components.set(declaration.name.text, component);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
ts.forEachChild(node, collect);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
collect(source);
|
|
35
|
-
const changed = [];
|
|
36
|
-
for (const entry of catalog.components) {
|
|
37
|
-
const component = components.get(entry.react);
|
|
38
|
-
const parameter = component?.parameters[0];
|
|
39
|
-
if (!parameter) throw new Error(`Cannot find first React parameter for ${entry.react}`);
|
|
40
|
-
const signature = parameter.getText(source);
|
|
41
|
-
if (entry.api.reactSignature !== signature) {
|
|
42
|
-
changed.push(entry.react);
|
|
43
|
-
entry.api.reactSignature = signature;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (process.argv.includes("--check")) {
|
|
48
|
-
if (changed.length) {
|
|
49
|
-
console.error(`Catalog React signatures are stale: ${changed.join(", ")}`);
|
|
50
|
-
process.exitCode = 1;
|
|
51
|
-
} else console.log(`React signatures match all ${catalog.components.length} adapters.`);
|
|
52
|
-
} else if (changed.length) {
|
|
53
|
-
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
54
|
-
console.log(`Updated ${changed.length} React signatures: ${changed.join(", ")}`);
|
|
55
|
-
} else console.log(`React signatures already match all ${catalog.components.length} adapters.`);
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { access, readFile } from "node:fs/promises";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
-
const catalog = JSON.parse(await readFile(resolve(pkgDir, "components.json"), "utf8"));
|
|
7
|
-
const built = await access(resolve(pkgDir, "src/react.tsx")).then(() => false).catch(() => true);
|
|
8
|
-
const sourceDir = resolve(pkgDir, built ? "dist" : "src");
|
|
9
|
-
const adapter = await readFile(resolve(sourceDir, built ? "react.d.ts" : "react.tsx"), "utf8");
|
|
10
|
-
const barrel = await readFile(resolve(sourceDir, built ? "index.d.ts" : "index.ts"), "utf8");
|
|
11
|
-
const errors = [];
|
|
12
|
-
|
|
13
|
-
for (const component of catalog.components) {
|
|
14
|
-
if (typeof component.summary !== "string" || !component.summary.trim()) errors.push(`${component.react}: missing human-readable component summary`);
|
|
15
|
-
if (!component.api?.reactSignature || !Array.isArray(component.api.slots) || !Array.isArray(component.api.callbackProps)) errors.push(`${component.react}: missing agent API signature, slots, or callback props`);
|
|
16
|
-
const exportName = component.define ?? component.factory;
|
|
17
|
-
if (!component.reactOnly && !exportName) errors.push(`${component.react}: missing custom-element definition or native-element factory`);
|
|
18
|
-
const signature = component.api.reactRefType
|
|
19
|
-
? (() => {
|
|
20
|
-
const declaration = new RegExp(`export const ${component.react} = (?:/\\* @__PURE__ \\*/ )?React\\.forwardRef<${component.api.reactRefType},`);
|
|
21
|
-
const declarationIndex = adapter.search(declaration);
|
|
22
|
-
const generic = component.api.generic ? `<${component.api.generic}>` : "";
|
|
23
|
-
const forwardedRef = component.api.forwardRefParameterType ? `forwardedRef: ${component.api.forwardRefParameterType}` : "forwardedRef";
|
|
24
|
-
const functionSignature = `function ${component.react}${generic}(${component.api.reactSignature}, ${forwardedRef})`;
|
|
25
|
-
return declarationIndex >= 0 && adapter.indexOf(functionSignature, declarationIndex) >= declarationIndex;
|
|
26
|
-
})()
|
|
27
|
-
: adapter.includes(`export function ${component.react}${component.api.generic ? `<${component.api.generic}>` : ""}(${component.api.reactSignature})`);
|
|
28
|
-
if (!signature) errors.push(`${component.react}: catalog API signature is stale`);
|
|
29
|
-
if (!new RegExp(`\\b${component.react}\\b`).test(adapter)) errors.push(`${component.react}: missing React adapter export`);
|
|
30
|
-
for (const helper of component.api.reactHelpers ?? []) {
|
|
31
|
-
if (!new RegExp(`\\bexport\\s+(?:const|function|type|interface)\\s+${helper}\\b`).test(adapter)) errors.push(`${component.react}: missing catalogued React helper ${helper}`);
|
|
32
|
-
}
|
|
33
|
-
if (component.reactOnly) continue;
|
|
34
|
-
const source = await readFile(resolve(sourceDir, `elements/${component.module}.${built ? "js" : "ts"}`), "utf8").catch(() => null);
|
|
35
|
-
if (!source) { errors.push(`${component.tag}: missing source module ${component.module}`); continue; }
|
|
36
|
-
const literalRegistration = new RegExp(`customElements\\.define\\(["']${component.tag}["']`).test(source);
|
|
37
|
-
const sharedFactoryRegistration = source.includes(`"${component.tag}"`) && /customElements\.define\(tag,/.test(source);
|
|
38
|
-
const partRegistration = component.tag.startsWith("spui-teaching-popover-") && source.includes(`partTag("${component.tag.slice("spui-teaching-popover-".length)}")`) && /function definePart\(/.test(source) && /customElements\.define\(tag,/.test(source);
|
|
39
|
-
if (component.registered !== false && !literalRegistration && !sharedFactoryRegistration && !partRegistration && !(source.includes(`"${component.tag}"`) && /customElements\.define\(name/.test(source))) errors.push(`${component.tag}: missing custom-element definition`);
|
|
40
|
-
if (exportName && !new RegExp(`\\b${exportName}\\b`).test(barrel)) errors.push(`${exportName}: missing public element/factory export`);
|
|
41
|
-
}
|
|
42
|
-
if (!catalog.frameworkCompatibility?.spfx || catalog.components.length === 0) errors.push("Catalog requires SPFx compatibility and components.");
|
|
43
|
-
if (errors.length) { console.error(`Catalog check failed:\n- ${errors.join("\n- ")}`); process.exitCode = 1; }
|
|
44
|
-
else console.log(`Validated ${catalog.components.length} component entries against React adapters and custom-element exports.`);
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sharepoint-ui
|
|
3
|
-
description: Build or change accessible, theme-aware SharePoint Framework interfaces with @spfx-fast/sharepoint-ui. Use when an SPFx web part needs reusable controls, SharePoint-style cards, tokens, RTL, custom elements, or rich text saved through SharePoint.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# SharePoint UI component skill
|
|
7
|
-
|
|
8
|
-
Use the package manifest at `../../components.json` to find exact React names, custom-element tags, and leaf entry points. The source repository checks it with `npm run catalog:check --workspace @spfx-fast/sharepoint-ui`. Use the package's TypeScript declarations as the final API authority. Do not infer that an unlisted Fluent v9 component exists; the library is still growing toward parity.
|
|
9
|
-
|
|
10
|
-
Start with [`QUICKSTART.md`](../../QUICKSTART.md) for the shared composition contract and copy-ready examples. Use [choosing components](references/choosing-components.md) to select controls and [SharePoint patterns](references/sharepoint-patterns.md) for locale, RTL, forms, and persistence.
|
|
11
|
-
|
|
12
|
-
Prefer leaf imports. Keep SharePoint permissions, data fetching, and writes in an SPFx service. Controls emit state; they do not save records implicitly. Validate keyboard use, theme contrast on the target surface, and RTL when relevant.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Choosing components
|
|
2
|
-
|
|
3
|
-
- Actions: `Button`.
|
|
4
|
-
- People and entities: `Avatar` with a name and optional image; use theme-derived `color="colorful"` for stable initials colors.
|
|
5
|
-
- Repeated SharePoint records: `ViewCard` for document, person, task, approval, or list-item content; compose details through slots.
|
|
6
|
-
- Filter and selected-value chips: use `Tag` inside a labelled `TagGroup`; set `dismissible` and update the backing array in `onDismiss`.
|
|
7
|
-
- Searchable multi-select values: use `TagPicker` with `TagPickerOption` children, stable values, and a controlled `selectedValues` array.
|
|
8
|
-
- User-provided scores: use `Rating` with a visible label and controlled `value`; set `step={0.5}` when half-star scores are useful.
|
|
9
|
-
- Short text and multiline text: `TextField` and `TextArea`.
|
|
10
|
-
- Typography and semantic labels: `Text` for heading/body/emphasis styles, and `Label` for form-control labels. Non-React code can use `spui-text` with `defineText()`; use the `createLabel()` factory when native `for` association is needed.
|
|
11
|
-
- Fixed choices: `Select` or `RadioGroup`.
|
|
12
|
-
- Searchable choices: `ComboBox` and `ComboOption`.
|
|
13
|
-
- Numeric/date/range values: `SpinButton`, `DatePicker`, and `Slider`.
|
|
14
|
-
- Rich formatted notes: `RichTextEditor`; it is a lightweight SharePoint-oriented editor, not a collaborative document surface.
|
|
15
|
-
- Navigation and disclosure: `Link` for URLs, `Divider` to separate content groups, and `Accordion` with `AccordionItem` for progressive disclosure. Prefer an explicit link name and preserve native anchor behavior.
|
|
16
|
-
- Persistent feedback: `Alert` for live status/urgent information; `MessageBar` for page-level notices. Use `Skeleton` for loading placeholders, and `Badge`, `Spinner`, or `ProgressBar` for compact status/progress.
|
|
17
|
-
- Brief feedback: mount one `Toaster` near the app root and call `dispatchToast` from `useToastController()`. Use persistent `Alert`/`MessageBar` content as well when the user must be able to return to the information later.
|
|
18
|
-
- Confirmation: `Dialog`.
|
|
19
|
-
|
|
20
|
-
Import React adapters from `@spfx-fast/sharepoint-ui/react`. For non-React use, import the matching leaf module, call its `define*()` function once, and use the tag from `components.json`; `Label` is the exception because its factory returns a native `<label>`. Check the source catalog before substituting a native element for a missing library family.
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# SharePoint patterns
|
|
2
|
-
|
|
3
|
-
## Theme and layout
|
|
4
|
-
|
|
5
|
-
Wrap a React surface in `SharePointUIProvider` and pass the SharePoint `ThemeProvider` values through `applySharePointTheme()` at the web-part boundary. Set the provider `lang`; it derives `dir` from the BCP 47 language tag. Set `dir` explicitly only to override that direction. Use tokens and CSS parts to customize; avoid copying a component's internal markup. Glass derives its values from the selected SharePoint/base appearance.
|
|
6
|
-
|
|
7
|
-
## Forms and events
|
|
8
|
-
|
|
9
|
-
Give every field a visible label. Use controlled values when the surrounding form owns state and wire the component's `onValueChange`/`onCheckedChange` callback. Native form behavior is used where possible; custom elements use `ElementInternals` where supported. Verify browser form reset and disabled behavior for the exact field before depending on it.
|
|
10
|
-
|
|
11
|
-
## SharePoint persistence
|
|
12
|
-
|
|
13
|
-
Use SPFx `SPHttpClient` or the project's existing data service for SharePoint requests. Show loading, success, and error states. Read important writes back and compare the saved result. `RichTextEditor` returns allowlist-normalized HTML; never render arbitrary saved HTML with unsanitized `innerHTML`.
|
|
14
|
-
|
|
15
|
-
## Agent handoff
|
|
16
|
-
|
|
17
|
-
State which controls were selected and why, list data fields and write locations, theme/locale assumptions, and the keyboard or tenant checks performed. Treat browser timing, host heap, and page long tasks as host-inclusive unless measured in isolation.
|