@stonedogcode/style 0.19.0 → 0.20.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/package.json +1 -1
- package/src/components/StyledImageUpload.tsx +335 -0
- package/src/components/StyledTable.tsx +222 -0
- package/src/index.ts +22 -0
package/package.json
CHANGED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { styled, type HTMLStyledProps } from "styled-system/jsx";
|
|
5
|
+
import { log } from "../config/logger";
|
|
6
|
+
import StyledButton from "./StyledButton";
|
|
7
|
+
import StyledText from "./StyledText";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A file dropzone with image previews.
|
|
11
|
+
*
|
|
12
|
+
* Extracted from HopperGuard, where it replaced Chakra's `FileUpload` compound
|
|
13
|
+
* component. Chakra supplied Root/HiddenInput/Dropzone/Trigger/ItemGroup/Item/
|
|
14
|
+
* ItemPreviewImage/ItemDeleteTrigger plus `useFileUploadContext`; all of it is
|
|
15
|
+
* reconstructed here on a local context, because the pieces share state (the
|
|
16
|
+
* accepted files) and cannot be swapped one at a time.
|
|
17
|
+
*
|
|
18
|
+
* ## What must not be lost
|
|
19
|
+
*
|
|
20
|
+
* These are pinned by the originating app's e2e spec, and each one is easy to
|
|
21
|
+
* break while the component still looks right:
|
|
22
|
+
*
|
|
23
|
+
* - a real `<input type="file">` stays ATTACHED to the DOM
|
|
24
|
+
* - the dropzone is reachable and operable BY KEYBOARD — so the trigger is a
|
|
25
|
+
* real `<button>`, never a div with a click handler
|
|
26
|
+
* - the dropzone announces itself IN WORDS, not by icon alone
|
|
27
|
+
* - selecting a file does not tear the widget down
|
|
28
|
+
*
|
|
29
|
+
* ## Icons
|
|
30
|
+
*
|
|
31
|
+
* This package ships no artwork by policy (see `StyledAlert`), so both glyph
|
|
32
|
+
* slots default to nothing and the words carry the meaning on their own. Pass
|
|
33
|
+
* your own nodes to restore an icon set — HopperGuard's facade passes
|
|
34
|
+
* `react-icons`' `LuFileImage` and `LuX`, which is what it rendered before the
|
|
35
|
+
* extraction.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
interface FileUploadContextValue {
|
|
39
|
+
acceptedFiles: File[];
|
|
40
|
+
openPicker: () => void;
|
|
41
|
+
removeFile: (file: File) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FileUploadContext = React.createContext<FileUploadContextValue | null>(
|
|
45
|
+
null,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function useFileUploadContext(): FileUploadContextValue {
|
|
49
|
+
const ctx = React.useContext(FileUploadContext);
|
|
50
|
+
if (!ctx) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"useFileUploadContext must be used inside StyledImageUpload",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return ctx;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const PandaVStack = styled("div", {
|
|
59
|
+
base: { display: "flex", flexDirection: "column", alignItems: "center" },
|
|
60
|
+
});
|
|
61
|
+
const PandaItemGroup = styled("div", {
|
|
62
|
+
base: {
|
|
63
|
+
display: "flex",
|
|
64
|
+
flexWrap: "wrap",
|
|
65
|
+
gap: "2",
|
|
66
|
+
justifyContent: "center",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const PandaItem = styled("div", { base: { position: "relative" } });
|
|
70
|
+
|
|
71
|
+
/** Chakra's `Float` — the delete control at the item's top-end corner. */
|
|
72
|
+
const PandaFloat = styled("div", {
|
|
73
|
+
base: {
|
|
74
|
+
position: "absolute",
|
|
75
|
+
top: "0",
|
|
76
|
+
right: "0",
|
|
77
|
+
transform: "translate(40%, -40%)",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `boxSize="4"` and `layerStyle="fill.solid"` were Chakra props with no Panda
|
|
83
|
+
* equivalent — they do not exist on a `styled("button")` and the type-check
|
|
84
|
+
* rejects both. Their effect is reproduced as real styles.
|
|
85
|
+
*
|
|
86
|
+
* The colours are tokens, never literals, so the control follows the host's
|
|
87
|
+
* theme and colour mode.
|
|
88
|
+
*/
|
|
89
|
+
const PandaDeleteTrigger = styled("button", {
|
|
90
|
+
base: {
|
|
91
|
+
display: "inline-flex",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
justifyContent: "center",
|
|
94
|
+
width: "4",
|
|
95
|
+
height: "4",
|
|
96
|
+
borderRadius: "full",
|
|
97
|
+
cursor: "pointer",
|
|
98
|
+
lineHeight: "1",
|
|
99
|
+
bg: "boxBgPrimary",
|
|
100
|
+
color: "textPrimary",
|
|
101
|
+
borderWidth: "1px",
|
|
102
|
+
borderStyle: "solid",
|
|
103
|
+
borderColor: "borderBgPrimary",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const PandaDropzone = styled("div", {
|
|
108
|
+
base: {
|
|
109
|
+
display: "flex",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
justifyContent: "center",
|
|
112
|
+
borderWidth: "1px",
|
|
113
|
+
borderStyle: "dashed",
|
|
114
|
+
borderColor: "borderBgPrimary",
|
|
115
|
+
borderRadius: "md",
|
|
116
|
+
padding: "4",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const PandaDropzoneContent = styled("div", {
|
|
121
|
+
base: {
|
|
122
|
+
display: "flex",
|
|
123
|
+
flexDirection: "column",
|
|
124
|
+
alignItems: "center",
|
|
125
|
+
justifyContent: "center",
|
|
126
|
+
gap: "2",
|
|
127
|
+
textAlign: "center",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The preview image.
|
|
133
|
+
*
|
|
134
|
+
* A plain `<img>` on purpose: the source is an object URL for a file the user
|
|
135
|
+
* just picked, so there is nothing for a framework image component to optimise
|
|
136
|
+
* — it has no intrinsic dimensions to read, no remote host to whitelist, and no
|
|
137
|
+
* cacheable URL. (In the originating Next.js app this needed a lint
|
|
138
|
+
* suppression; this package has no such rule, so it needs none.)
|
|
139
|
+
*/
|
|
140
|
+
function PreviewImage({ file }: { file: File }) {
|
|
141
|
+
const [src, setSrc] = React.useState<string>();
|
|
142
|
+
/**
|
|
143
|
+
* Creating the object URL in an effect keyed on the File — rather than inline
|
|
144
|
+
* during render — is what stops a blob leaking on every re-render, and the
|
|
145
|
+
* cleanup is what stops one leaking when the preview is removed.
|
|
146
|
+
*/
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
const url = URL.createObjectURL(file);
|
|
149
|
+
setSrc(url);
|
|
150
|
+
return () => URL.revokeObjectURL(url);
|
|
151
|
+
}, [file]);
|
|
152
|
+
if (!src) return null;
|
|
153
|
+
return (
|
|
154
|
+
<img
|
|
155
|
+
src={src}
|
|
156
|
+
alt={file.name}
|
|
157
|
+
// Inline rather than a Panda rule: a size a consumer's Panda `include`
|
|
158
|
+
// glob might never extract is a size that silently does not apply.
|
|
159
|
+
style={{
|
|
160
|
+
borderRadius: "8px",
|
|
161
|
+
objectFit: "cover",
|
|
162
|
+
width: "96px",
|
|
163
|
+
height: "96px",
|
|
164
|
+
display: "block",
|
|
165
|
+
margin: "0 auto",
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface StyledImageUploadProps
|
|
172
|
+
extends Omit<HTMLStyledProps<"div">, "onChange"> {
|
|
173
|
+
/** Controlled-ish initial selection. Currently informational only. */
|
|
174
|
+
value?: File[];
|
|
175
|
+
onChange?: (files: File[]) => void;
|
|
176
|
+
/** `1` keeps the input single-select; above 1 sets `multiple`. */
|
|
177
|
+
maxFiles?: number;
|
|
178
|
+
accept?: string;
|
|
179
|
+
buttonText?: string;
|
|
180
|
+
dropzoneText?: string;
|
|
181
|
+
/** Decorative glyph inside the upload button. `aria-hidden`. */
|
|
182
|
+
fileIcon?: React.ReactNode;
|
|
183
|
+
/** Decorative glyph inside each remove button. `aria-hidden`. */
|
|
184
|
+
removeIcon?: React.ReactNode;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const FileUploadPreviewOnly = ({
|
|
188
|
+
removeIcon,
|
|
189
|
+
}: {
|
|
190
|
+
removeIcon?: React.ReactNode;
|
|
191
|
+
}) => {
|
|
192
|
+
const { acceptedFiles, removeFile } = useFileUploadContext();
|
|
193
|
+
if (acceptedFiles.length === 0) return null;
|
|
194
|
+
return (
|
|
195
|
+
<PandaItemGroup>
|
|
196
|
+
{acceptedFiles.map((file) => (
|
|
197
|
+
<PandaItem p="2" key={file.name}>
|
|
198
|
+
<PreviewImage file={file} />
|
|
199
|
+
<PandaFloat>
|
|
200
|
+
{/*
|
|
201
|
+
The accessible name is on the BUTTON and names the file, so the
|
|
202
|
+
glyph is decorative and hidden. A screen-reader user with three
|
|
203
|
+
previews needs to know which one this removes.
|
|
204
|
+
*/}
|
|
205
|
+
<PandaDeleteTrigger
|
|
206
|
+
type="button"
|
|
207
|
+
aria-label={`Remove ${file.name}`}
|
|
208
|
+
onClick={() => removeFile(file)}
|
|
209
|
+
>
|
|
210
|
+
{removeIcon ? (
|
|
211
|
+
<span aria-hidden="true">{removeIcon}</span>
|
|
212
|
+
) : null}
|
|
213
|
+
</PandaDeleteTrigger>
|
|
214
|
+
</PandaFloat>
|
|
215
|
+
</PandaItem>
|
|
216
|
+
))}
|
|
217
|
+
</PandaItemGroup>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const FileUploadDropzoneOnly = ({
|
|
222
|
+
buttonText,
|
|
223
|
+
dropzoneText,
|
|
224
|
+
fileIcon,
|
|
225
|
+
}: {
|
|
226
|
+
buttonText: string;
|
|
227
|
+
dropzoneText: string;
|
|
228
|
+
fileIcon?: React.ReactNode;
|
|
229
|
+
}) => {
|
|
230
|
+
const { openPicker } = useFileUploadContext();
|
|
231
|
+
return (
|
|
232
|
+
<PandaDropzone
|
|
233
|
+
width="100%"
|
|
234
|
+
cursor="pointer"
|
|
235
|
+
onClick={openPicker}
|
|
236
|
+
onDragOver={(e) => e.preventDefault()}
|
|
237
|
+
onDrop={(e) => e.preventDefault()}
|
|
238
|
+
>
|
|
239
|
+
<PandaDropzoneContent cursor="pointer">
|
|
240
|
+
<StyledText cursor="pointer">{dropzoneText}</StyledText>
|
|
241
|
+
{/*
|
|
242
|
+
A real <button>, not the dropzone div with a handler: the control must
|
|
243
|
+
be reachable and operable by KEYBOARD, and a div is neither focusable
|
|
244
|
+
nor Enter/Space-activated. `stopPropagation` keeps the click from also
|
|
245
|
+
reaching the dropzone and opening the picker twice.
|
|
246
|
+
*/}
|
|
247
|
+
<StyledButton
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={(e: React.MouseEvent) => {
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
openPicker();
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
{buttonText}
|
|
255
|
+
{fileIcon ? <span aria-hidden="true">{fileIcon}</span> : null}
|
|
256
|
+
</StyledButton>
|
|
257
|
+
</PandaDropzoneContent>
|
|
258
|
+
</PandaDropzone>
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export function StyledImageUpload({
|
|
263
|
+
onChange,
|
|
264
|
+
maxFiles = 1,
|
|
265
|
+
accept = "image/*",
|
|
266
|
+
buttonText = "Upload Image",
|
|
267
|
+
dropzoneText = "Drag and drop an image or",
|
|
268
|
+
fileIcon,
|
|
269
|
+
removeIcon,
|
|
270
|
+
...vStackProps
|
|
271
|
+
}: StyledImageUploadProps) {
|
|
272
|
+
log.trace("StyledImageUpload rendered");
|
|
273
|
+
const [acceptedFiles, setAcceptedFiles] = React.useState<File[]>([]);
|
|
274
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
275
|
+
|
|
276
|
+
const commit = React.useCallback(
|
|
277
|
+
(files: File[]) => {
|
|
278
|
+
const limited = maxFiles > 0 ? files.slice(0, maxFiles) : files;
|
|
279
|
+
setAcceptedFiles(limited);
|
|
280
|
+
onChange?.(limited);
|
|
281
|
+
},
|
|
282
|
+
[maxFiles, onChange],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const ctx = React.useMemo<FileUploadContextValue>(
|
|
286
|
+
() => ({
|
|
287
|
+
acceptedFiles,
|
|
288
|
+
openPicker: () => inputRef.current?.click(),
|
|
289
|
+
removeFile: (file) =>
|
|
290
|
+
commit(acceptedFiles.filter((f) => f.name !== file.name)),
|
|
291
|
+
}),
|
|
292
|
+
[acceptedFiles, commit],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<FileUploadContext.Provider value={ctx}>
|
|
297
|
+
{/*
|
|
298
|
+
Chakra's HiddenInput. `display: none` would DETACH it from the
|
|
299
|
+
accessibility tree; it is visually hidden the standard way instead so it
|
|
300
|
+
stays reachable and stays a real form control.
|
|
301
|
+
*/}
|
|
302
|
+
<input
|
|
303
|
+
ref={inputRef}
|
|
304
|
+
type="file"
|
|
305
|
+
accept={accept}
|
|
306
|
+
multiple={maxFiles > 1}
|
|
307
|
+
onChange={(e) => commit(Array.from(e.target.files ?? []))}
|
|
308
|
+
style={{
|
|
309
|
+
position: "absolute",
|
|
310
|
+
width: "1px",
|
|
311
|
+
height: "1px",
|
|
312
|
+
padding: 0,
|
|
313
|
+
margin: "-1px",
|
|
314
|
+
overflow: "hidden",
|
|
315
|
+
clip: "rect(0 0 0 0)",
|
|
316
|
+
whiteSpace: "nowrap",
|
|
317
|
+
border: 0,
|
|
318
|
+
}}
|
|
319
|
+
/>
|
|
320
|
+
<PandaVStack gap={2} {...vStackProps}>
|
|
321
|
+
{acceptedFiles.length === 0 ? (
|
|
322
|
+
<FileUploadDropzoneOnly
|
|
323
|
+
buttonText={buttonText}
|
|
324
|
+
dropzoneText={dropzoneText}
|
|
325
|
+
fileIcon={fileIcon}
|
|
326
|
+
/>
|
|
327
|
+
) : (
|
|
328
|
+
<FileUploadPreviewOnly removeIcon={removeIcon} />
|
|
329
|
+
)}
|
|
330
|
+
</PandaVStack>
|
|
331
|
+
</FileUploadContext.Provider>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export default StyledImageUpload;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { styled } from "styled-system/jsx";
|
|
5
|
+
import { log } from "../config/logger";
|
|
6
|
+
import StyledBox from "./StyledBox";
|
|
7
|
+
import StyledScrollbar from "./StyledScrollbar";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A data table that renders real table elements.
|
|
11
|
+
*
|
|
12
|
+
* Extracted from HopperGuard, where it began as Chakra's `Table.*` compound
|
|
13
|
+
* component. Chakra rendered real `<table>` markup and the app's e2e spec
|
|
14
|
+
* asserts `toHaveRole("table")`, so every part here is pinned to the semantic
|
|
15
|
+
* element Chakra produced rather than to a styled `<div>`. A div grid is
|
|
16
|
+
* pixel-identical and an accessibility regression: screen readers announce row
|
|
17
|
+
* and column position from the table role, and lose it entirely on divs.
|
|
18
|
+
*
|
|
19
|
+
* The compound shape is deliberate. `Header` takes a column list because that
|
|
20
|
+
* is the shape every consumer already had; the rest are thin passthroughs so a
|
|
21
|
+
* caller can drop to raw rows and cells whenever the list shape does not fit.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** A column for `StyledTable.Header`. Extra props reach the `<th>`. */
|
|
25
|
+
export interface ColumnDefinition
|
|
26
|
+
extends React.ComponentProps<typeof PandaTableColumnHeader> {
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PandaTableRoot = styled("table", {
|
|
32
|
+
base: {
|
|
33
|
+
borderCollapse: "collapse",
|
|
34
|
+
textAlign: "start",
|
|
35
|
+
verticalAlign: "top",
|
|
36
|
+
// Digits share a column width, so numeric cells line up down the table.
|
|
37
|
+
fontVariantNumeric: "lining-nums tabular-nums",
|
|
38
|
+
// MEASURED off the originating Chakra build with getComputedStyle, not
|
|
39
|
+
// derived from tokens: font 14px / line-height 20px, cells 12px on every
|
|
40
|
+
// side, a 1px rule under each cell. Reasoning from the token scale instead
|
|
41
|
+
// (fontSize md, py 2) put the table 51px too tall — the font, not the
|
|
42
|
+
// padding, drove the difference.
|
|
43
|
+
fontSize: "14px",
|
|
44
|
+
lineHeight: "20px",
|
|
45
|
+
},
|
|
46
|
+
variants: {
|
|
47
|
+
/**
|
|
48
|
+
* `size` was a Chakra recipe prop. Without a variant declared here it would
|
|
49
|
+
* fall through to the DOM as an invalid `size` attribute on `<table>`.
|
|
50
|
+
*
|
|
51
|
+
* Only `md` is measured against the original — it is what the visual
|
|
52
|
+
* baseline captured. `sm` and `lg` are proportional and UNVERIFIED; treat
|
|
53
|
+
* them as a reasonable scale rather than as a reproduction of anything.
|
|
54
|
+
*/
|
|
55
|
+
size: {
|
|
56
|
+
sm: { "& th, & td": { padding: "8px" } },
|
|
57
|
+
md: { "& th, & td": { padding: "12px" } },
|
|
58
|
+
lg: { "& th, & td": { padding: "16px" } },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: { size: "md" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const PandaTableHeader = styled("thead");
|
|
65
|
+
const PandaTableBody = styled("tbody");
|
|
66
|
+
const PandaTableFooter = styled("tfoot");
|
|
67
|
+
const PandaTableRow = styled("tr");
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The 1px rule under each cell.
|
|
71
|
+
*
|
|
72
|
+
* Chakra's own CSS drew this. `border="sm"` on the cell emits NOTHING — an
|
|
73
|
+
* empty computed `border` shorthand on a real `<td>` — so it is restored
|
|
74
|
+
* explicitly.
|
|
75
|
+
*
|
|
76
|
+
* `borderBgPrimary` is the token, never a palette literal: a hardcoded
|
|
77
|
+
* `neutral.200` is invisible to the host's theme and does not follow the colour
|
|
78
|
+
* mode, which is the whole reason this package refuses literal colours.
|
|
79
|
+
*
|
|
80
|
+
* ## Longhands, and why the obvious shorthand is wrong
|
|
81
|
+
*
|
|
82
|
+
* The originating app wrote `borderBottom: "1px solid"` alongside
|
|
83
|
+
* `borderColor: <token>`. That looks equivalent and is not: `border-bottom` is
|
|
84
|
+
* a SHORTHAND, so it also sets `border-bottom-color`, and omitting the colour
|
|
85
|
+
* resets it to its initial value — `currentColor`. Whichever declaration Panda
|
|
86
|
+
* emits second wins, so the cell rule painted the text colour (black on a light
|
|
87
|
+
* theme) instead of the token, on every table.
|
|
88
|
+
*
|
|
89
|
+
* That is a false-pass waiting to happen, because black IS a real colour: a
|
|
90
|
+
* test asserting only "the border resolved to something" goes green on it.
|
|
91
|
+
* `StyledTable.ct.tsx` re-points the custom property and asserts the border
|
|
92
|
+
* follows, which is the assertion that actually distinguishes the two.
|
|
93
|
+
*/
|
|
94
|
+
const CELL_RULE = {
|
|
95
|
+
borderBottomWidth: "1px",
|
|
96
|
+
borderBottomStyle: "solid",
|
|
97
|
+
borderBottomColor: "borderBgPrimary",
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
const PandaTableColumnHeader = styled("th", {
|
|
101
|
+
base: { fontWeight: "medium", textAlign: "start", ...CELL_RULE },
|
|
102
|
+
});
|
|
103
|
+
const PandaTableCell = styled("td", { base: { ...CELL_RULE } });
|
|
104
|
+
const PandaTableCaption = styled("caption");
|
|
105
|
+
|
|
106
|
+
export type StyledTableProps = React.ComponentProps<typeof PandaTableRoot>;
|
|
107
|
+
|
|
108
|
+
const StyledTable: React.FC<StyledTableProps> = ({ children, ...props }) => {
|
|
109
|
+
log.trace("StyledTable rendered");
|
|
110
|
+
return (
|
|
111
|
+
// `overflow: hidden` on the outer box clips the scroll container's corners
|
|
112
|
+
// to the box radius; the scrollbar inside is what actually scrolls.
|
|
113
|
+
<StyledBox
|
|
114
|
+
overflow="hidden"
|
|
115
|
+
data-testid="styled-table-container"
|
|
116
|
+
border={0}
|
|
117
|
+
py={0}
|
|
118
|
+
px={0}
|
|
119
|
+
>
|
|
120
|
+
<StyledScrollbar p={0} data-testid="styled-table-scrollbar" border={0}>
|
|
121
|
+
<PandaTableRoot {...props} data-testid="styled-table-root">
|
|
122
|
+
{children}
|
|
123
|
+
</PandaTableRoot>
|
|
124
|
+
</StyledScrollbar>
|
|
125
|
+
</StyledBox>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export interface StyledTableHeaderProps {
|
|
130
|
+
columns: ColumnDefinition[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const StyledHeader: React.FC<StyledTableHeaderProps> = ({ columns }) => (
|
|
134
|
+
<PandaTableHeader>
|
|
135
|
+
<PandaTableRow>
|
|
136
|
+
{columns.map(({ key, label, ...rest }) => (
|
|
137
|
+
<PandaTableColumnHeader key={key} {...rest}>
|
|
138
|
+
{label}
|
|
139
|
+
</PandaTableColumnHeader>
|
|
140
|
+
))}
|
|
141
|
+
</PandaTableRow>
|
|
142
|
+
</PandaTableHeader>
|
|
143
|
+
);
|
|
144
|
+
StyledHeader.displayName = "StyledTable.Header";
|
|
145
|
+
|
|
146
|
+
export interface StyledTableBodyProps {
|
|
147
|
+
children: React.ReactNode;
|
|
148
|
+
header?: React.ReactNode;
|
|
149
|
+
footer?: React.ReactNode;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `header` and `footer` render as siblings, not children — `<thead>` and
|
|
154
|
+
* `<tfoot>` are invalid inside `<tbody>`, and nesting them there is silently
|
|
155
|
+
* reparented by the browser rather than reported.
|
|
156
|
+
*/
|
|
157
|
+
const StyledBody: React.FC<StyledTableBodyProps> = ({
|
|
158
|
+
children,
|
|
159
|
+
header,
|
|
160
|
+
footer,
|
|
161
|
+
}) => (
|
|
162
|
+
<>
|
|
163
|
+
{header}
|
|
164
|
+
<PandaTableBody>{children}</PandaTableBody>
|
|
165
|
+
{footer}
|
|
166
|
+
</>
|
|
167
|
+
);
|
|
168
|
+
StyledBody.displayName = "StyledTable.Body";
|
|
169
|
+
|
|
170
|
+
type StyledTableFooterProps = React.ComponentProps<typeof PandaTableFooter>;
|
|
171
|
+
const StyledFooter: React.FC<StyledTableFooterProps> = (props) => (
|
|
172
|
+
<PandaTableFooter {...props} />
|
|
173
|
+
);
|
|
174
|
+
StyledFooter.displayName = "StyledTable.Footer";
|
|
175
|
+
|
|
176
|
+
type StyledTableRowProps = React.ComponentProps<typeof PandaTableRow>;
|
|
177
|
+
const StyledRow: React.FC<StyledTableRowProps> = (props) => (
|
|
178
|
+
<PandaTableRow {...props} />
|
|
179
|
+
);
|
|
180
|
+
StyledRow.displayName = "StyledTable.Row";
|
|
181
|
+
|
|
182
|
+
type StyledColumnHeaderProps = React.ComponentProps<
|
|
183
|
+
typeof PandaTableColumnHeader
|
|
184
|
+
>;
|
|
185
|
+
const StyledColumnHeader: React.FC<StyledColumnHeaderProps> = (props) => (
|
|
186
|
+
<PandaTableColumnHeader {...props} />
|
|
187
|
+
);
|
|
188
|
+
StyledColumnHeader.displayName = "StyledTable.ColumnHeader";
|
|
189
|
+
|
|
190
|
+
type StyledTableCellProps = React.ComponentProps<typeof PandaTableCell>;
|
|
191
|
+
const StyledCell: React.FC<StyledTableCellProps> = (props) => (
|
|
192
|
+
<PandaTableCell {...props} />
|
|
193
|
+
);
|
|
194
|
+
StyledCell.displayName = "StyledTable.Cell";
|
|
195
|
+
|
|
196
|
+
type StyledTableCaptionProps = React.ComponentProps<typeof PandaTableCaption>;
|
|
197
|
+
const StyledCaption: React.FC<StyledTableCaptionProps> = (props) => (
|
|
198
|
+
<PandaTableCaption {...props} />
|
|
199
|
+
);
|
|
200
|
+
StyledCaption.displayName = "StyledTable.Caption";
|
|
201
|
+
|
|
202
|
+
interface StyledTableComponent extends React.FC<StyledTableProps> {
|
|
203
|
+
Header: typeof StyledHeader;
|
|
204
|
+
Body: typeof StyledBody;
|
|
205
|
+
Footer: typeof StyledFooter;
|
|
206
|
+
Row: typeof StyledRow;
|
|
207
|
+
ColumnHeader: typeof StyledColumnHeader;
|
|
208
|
+
Cell: typeof StyledCell;
|
|
209
|
+
Caption: typeof StyledCaption;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const StyledTableExport = StyledTable as StyledTableComponent;
|
|
213
|
+
StyledTableExport.Header = StyledHeader;
|
|
214
|
+
StyledTableExport.Body = StyledBody;
|
|
215
|
+
StyledTableExport.Footer = StyledFooter;
|
|
216
|
+
StyledTableExport.Row = StyledRow;
|
|
217
|
+
StyledTableExport.ColumnHeader = StyledColumnHeader;
|
|
218
|
+
StyledTableExport.Cell = StyledCell;
|
|
219
|
+
StyledTableExport.Caption = StyledCaption;
|
|
220
|
+
|
|
221
|
+
export { StyledTableExport as StyledTable };
|
|
222
|
+
export default StyledTableExport;
|
package/src/index.ts
CHANGED
|
@@ -210,6 +210,16 @@ export {
|
|
|
210
210
|
} from "./components/StyledFieldHelp";
|
|
211
211
|
export type { StyledFieldHelpProps } from "./components/StyledFieldHelp";
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* A file dropzone with image previews. Ships no artwork: both glyph slots
|
|
215
|
+
* default to nothing and the host passes its own icon set if it wants one.
|
|
216
|
+
*/
|
|
217
|
+
export {
|
|
218
|
+
default as StyledImageUpload,
|
|
219
|
+
StyledImageUpload as ImageUpload,
|
|
220
|
+
} from "./components/StyledImageUpload";
|
|
221
|
+
export type { StyledImageUploadProps } from "./components/StyledImageUpload";
|
|
222
|
+
|
|
213
223
|
// ---------------------------------------------------------------------------
|
|
214
224
|
// Components that were blocked on a runtime dependency until NEH-430 gave each
|
|
215
225
|
// a seam with a working default. None of them adds a dependency; the host
|
|
@@ -316,6 +326,18 @@ export { DL_VARIANTS } from "./components/StyledDefinitionList";
|
|
|
316
326
|
export { default as StyledSparkLine } from "./components/StyledSparkLine";
|
|
317
327
|
export type { StyledSparkLineProps } from "./components/StyledSparkLine";
|
|
318
328
|
|
|
329
|
+
/**
|
|
330
|
+
* A data table that renders real `<table>` markup rather than a div grid — the
|
|
331
|
+
* table role is what carries row and column position to a screen reader.
|
|
332
|
+
*/
|
|
333
|
+
export { default as StyledTable, StyledTable as Table } from "./components/StyledTable";
|
|
334
|
+
export type {
|
|
335
|
+
StyledTableProps,
|
|
336
|
+
StyledTableHeaderProps,
|
|
337
|
+
StyledTableBodyProps,
|
|
338
|
+
ColumnDefinition,
|
|
339
|
+
} from "./components/StyledTable";
|
|
340
|
+
|
|
319
341
|
// ---------------------------------------------------------------------------
|
|
320
342
|
// Notifications
|
|
321
343
|
// ---------------------------------------------------------------------------
|