gbs-add-block 1.2.14 → 1.2.16
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/README.md +2 -2
- package/index.cjs +219 -21
- package/package.json +1 -1
- package/source/beta-components/breadcrumb/README.md +34 -0
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
- package/source/beta-components/breadcrumb/core/index.ts +4 -0
- package/source/beta-components/breadcrumb/core/trail.ts +42 -0
- package/source/beta-components/breadcrumb/core/types.ts +20 -0
- package/source/beta-components/breadcrumb/index.ts +5 -0
- package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
- package/source/beta-components/breadcrumb/react/locale.ts +6 -0
- package/source/beta-components/breadcrumb/react/props.ts +14 -0
- package/source/beta-components/breadcrumb/styles.css +139 -0
- package/source/beta-components/button/README.md +32 -0
- package/source/beta-components/button/__tests__/core.test.ts +31 -0
- package/source/beta-components/button/core/index.ts +3 -0
- package/source/beta-components/button/core/state.ts +16 -0
- package/source/beta-components/button/core/types.ts +28 -0
- package/source/beta-components/button/index.ts +5 -0
- package/source/beta-components/button/react/Button.tsx +173 -0
- package/source/beta-components/button/react/locale.ts +5 -0
- package/source/beta-components/button/react/props.ts +23 -0
- package/source/beta-components/button/styles.css +233 -0
- package/source/beta-components/checkbox/README.md +36 -0
- package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
- package/source/beta-components/checkbox/core/group.ts +30 -0
- package/source/beta-components/checkbox/core/index.ts +3 -0
- package/source/beta-components/checkbox/core/types.ts +17 -0
- package/source/beta-components/checkbox/index.ts +9 -0
- package/source/beta-components/checkbox/react/Checkbox.tsx +147 -0
- package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
- package/source/beta-components/checkbox/react/context.ts +16 -0
- package/source/beta-components/checkbox/react/locale.ts +5 -0
- package/source/beta-components/checkbox/react/props.ts +6 -0
- package/source/beta-components/checkbox/styles.css +212 -0
- package/source/beta-components/combobox/README.md +3 -3
- package/source/beta-components/combobox/react/MultiSelect.tsx +265 -263
- package/source/beta-components/combobox/react/Popover.tsx +38 -70
- package/source/beta-components/combobox/react/Select.tsx +200 -198
- package/source/beta-components/combobox/react/icons.tsx +6 -29
- package/source/beta-components/combobox/react/props.ts +4 -2
- package/source/beta-components/combobox/styles.css +18 -18
- package/source/beta-components/data-grid/react/Popover.tsx +35 -0
- package/source/beta-components/{datagrid → data-grid}/react/context.ts +2 -2
- package/source/beta-components/{datagrid → data-grid}/react/icons.tsx +8 -35
- package/source/beta-components/{datagrid → data-grid}/styles.css +24 -24
- package/source/beta-components/{datepicker → date-picker}/README.md +3 -3
- package/source/beta-components/{datepicker → date-picker}/react/DatePicker.tsx +249 -249
- package/source/beta-components/{datepicker → date-picker}/react/DateRangePicker.tsx +291 -291
- package/source/beta-components/date-picker/react/Popover.tsx +45 -0
- package/source/beta-components/date-picker/react/icons.tsx +15 -0
- package/source/beta-components/{datepicker → date-picker}/react/props.ts +2 -2
- package/source/beta-components/{datepicker → date-picker}/styles.css +18 -18
- package/source/beta-components/dialog/react/icons.tsx +16 -47
- package/source/beta-components/dialog/react/props.ts +2 -2
- package/source/beta-components/dialog/styles.css +21 -21
- package/source/beta-components/{fileuploader → file-uploader}/react/FileUploader.tsx +511 -515
- package/source/beta-components/{fileuploader → file-uploader}/react/icons.tsx +7 -27
- package/source/beta-components/{fileuploader → file-uploader}/react/props.ts +2 -2
- package/source/beta-components/{fileuploader → file-uploader}/styles.css +17 -17
- package/source/beta-components/input/core/count.ts +3 -12
- package/source/beta-components/input/react/Input.tsx +219 -219
- package/source/beta-components/input/react/OtpInput.tsx +256 -256
- package/source/beta-components/input/react/icons.tsx +3 -21
- package/source/beta-components/input/react/props.ts +2 -2
- package/source/beta-components/input/styles.css +15 -15
- package/source/beta-components/modal/react/Modal.tsx +242 -238
- package/source/beta-components/modal/react/icons.tsx +1 -19
- package/source/beta-components/modal/react/props.ts +2 -2
- package/source/beta-components/modal/styles.css +17 -13
- package/source/beta-components/shared/README.md +54 -0
- package/source/beta-components/shared/__tests__/boundaries.test.ts +95 -0
- package/source/beta-components/shared/__tests__/core.test.ts +55 -0
- package/source/beta-components/shared/core/cx.ts +9 -0
- package/source/beta-components/shared/core/field.ts +35 -0
- package/source/beta-components/shared/core/text.ts +14 -0
- package/source/beta-components/shared/index.ts +7 -0
- package/source/beta-components/shared/react/Popover.tsx +147 -0
- package/source/beta-components/shared/react/icons.tsx +74 -0
- package/source/beta-components/shared/react/useControllableState.ts +42 -0
- package/source/beta-components/shared/version.json +4 -0
- package/source/beta-components/shared/version.ts +6 -0
- package/source/beta-components/spinner/README.md +29 -0
- package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
- package/source/beta-components/spinner/core/index.ts +3 -0
- package/source/beta-components/spinner/core/types.ts +28 -0
- package/source/beta-components/spinner/core/visibility.ts +34 -0
- package/source/beta-components/spinner/index.ts +6 -0
- package/source/beta-components/spinner/react/Spinner.tsx +96 -0
- package/source/beta-components/spinner/react/locale.ts +5 -0
- package/source/beta-components/spinner/react/props.ts +4 -0
- package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
- package/source/beta-components/spinner/styles.css +167 -0
- package/source/beta-components/tabs/README.md +32 -0
- package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
- package/source/beta-components/tabs/core/index.ts +3 -0
- package/source/beta-components/tabs/core/navigation.ts +54 -0
- package/source/beta-components/tabs/core/types.ts +16 -0
- package/source/beta-components/tabs/index.ts +5 -0
- package/source/beta-components/tabs/react/Tabs.tsx +258 -0
- package/source/beta-components/tabs/react/context.ts +21 -0
- package/source/beta-components/tabs/styles.css +257 -0
- package/source/beta-components/textarea/core/count.ts +3 -12
- package/source/beta-components/textarea/react/Textarea.tsx +208 -209
- package/source/beta-components/textarea/react/props.ts +2 -2
- package/source/beta-components/textarea/styles.css +12 -12
- package/source/beta-components/toaster/react/icons.tsx +19 -52
- package/source/beta-components/toaster/react/props.ts +2 -2
- package/source/beta-components/toaster/styles.css +18 -18
- package/source/beta-components/datagrid/react/Popover.tsx +0 -79
- package/source/beta-components/datepicker/react/Popover.tsx +0 -87
- package/source/beta-components/datepicker/react/icons.tsx +0 -32
- /package/source/beta-components/{datagrid → data-grid}/README.md +0 -0
- /package/source/beta-components/{datagrid → data-grid}/__tests__/core.test.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/__tests__/export.test.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/columnHelper.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/columns.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/filtering.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/grid.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/index.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/rows.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/sorting.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/state.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/store.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/types.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/values.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/core/virtual.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/export/csv.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/export/download.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/export/pdf.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/export/table.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/export/xlsx.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/export/zip.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/index.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/Cell.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/ColumnMenu.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/DataGrid.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/FilterForm.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/HeaderRow.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/Pagination.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/Row.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/Toolbar.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/Viewport.tsx +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/hooks.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/keyboard.ts +0 -0
- /package/source/beta-components/{datagrid → data-grid}/react/locale.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/__tests__/core.test.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/core/calendar.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/core/format.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/core/index.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/core/types.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/index.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/react/Calendar.tsx +0 -0
- /package/source/beta-components/{datepicker → date-picker}/react/CalendarPanel.tsx +0 -0
- /package/source/beta-components/{datepicker → date-picker}/react/MonthYearPanels.tsx +0 -0
- /package/source/beta-components/{datepicker → date-picker}/react/locale.ts +0 -0
- /package/source/beta-components/{datepicker → date-picker}/react/useDatePicker.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/README.md +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/__tests__/core.test.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/core/format.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/core/index.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/core/store.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/core/transport.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/core/types.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/core/validate.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/index.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/react/FileRow.tsx +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/react/locale.ts +0 -0
- /package/source/beta-components/{fileuploader → file-uploader}/react/useFileUploader.ts +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# GBS Building Blocks 2.0 (v1.2.
|
|
1
|
+
# GBS Building Blocks 2.0 (v1.2.16)
|
|
2
2
|
|
|
3
3
|
Latest and upgraded version of GBS building blocks with headless UI and removed dependencies.
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ For detailed documentation on usage and props, Please visit: [Building Block Doc
|
|
|
10
10
|
|
|
11
11
|
New Beta Components are avilable for testing and feedback, Please visit: [Building Block Documentation v2.0.0 beta](https://gramprokit.vercel.app/2.0.0-beta)
|
|
12
12
|
|
|
13
|
-
## What's New 🎉 (Ver 1.2.
|
|
13
|
+
## What's New 🎉 (Ver 1.2.16)
|
|
14
14
|
|
|
15
15
|
- Update Candidate for next major change 2.0.0
|
|
16
16
|
|
package/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs-extra");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const crypto = require("crypto");
|
|
5
6
|
const yargs = require("yargs/yargs");
|
|
6
7
|
const { hideBin } = require("yargs/helpers");
|
|
7
8
|
const readline = require("readline");
|
|
@@ -45,7 +46,18 @@ const CONFIG = {
|
|
|
45
46
|
"Input",
|
|
46
47
|
"Modal",
|
|
47
48
|
"Textarea",
|
|
49
|
+
"Button",
|
|
50
|
+
"Breadcrumb",
|
|
51
|
+
"Checkbox",
|
|
52
|
+
"Tabs",
|
|
53
|
+
"Spinner",
|
|
48
54
|
],
|
|
55
|
+
// Beta folder names that are not simply the lowercased component name.
|
|
56
|
+
betaFolders: {
|
|
57
|
+
DataGrid: "data-grid",
|
|
58
|
+
DatePicker: "date-picker",
|
|
59
|
+
FileUploader: "file-uploader",
|
|
60
|
+
},
|
|
49
61
|
// Define component dependencies
|
|
50
62
|
dependencies: {
|
|
51
63
|
FormRenderer: ["Select", "MultiSelect", "Input", "DatePicker"],
|
|
@@ -57,6 +69,24 @@ const SOURCE_PATH = path.join(__dirname, "source", "components");
|
|
|
57
69
|
const BETA_SOURCE_PATH = path.join(__dirname, "source", "beta-components");
|
|
58
70
|
const DEFAULT_DEST_PATH = path.join(process.cwd(), "component-lib");
|
|
59
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Every beta component imports its shared helpers from `../../shared`, so this
|
|
74
|
+
* folder is installed alongside whatever the user picked. It is not a component
|
|
75
|
+
* and never appears in the component list.
|
|
76
|
+
*/
|
|
77
|
+
const SHARED_DIR = "shared";
|
|
78
|
+
const MANIFEST_FILE = ".install-manifest.json";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Tests belong to this repo, not to the projects we install into: they import
|
|
82
|
+
* vitest, which the project has no reason to have, and the shared folder's
|
|
83
|
+
* boundary tests would run against whatever else is in the user's
|
|
84
|
+
* component-lib.
|
|
85
|
+
*/
|
|
86
|
+
const TESTS_DIR = "__tests__";
|
|
87
|
+
const isTestPath = (filePath) =>
|
|
88
|
+
filePath.split(/[\\/]/).includes(TESTS_DIR);
|
|
89
|
+
|
|
60
90
|
const normalizeComponent = (component, availableComponents) =>
|
|
61
91
|
availableComponents.find(
|
|
62
92
|
(available) => available.toLowerCase() === component.toLowerCase(),
|
|
@@ -67,6 +97,10 @@ const getAvailableComponents = (beta = false) =>
|
|
|
67
97
|
|
|
68
98
|
const getSourcePath = (beta = false) => (beta ? BETA_SOURCE_PATH : SOURCE_PATH);
|
|
69
99
|
|
|
100
|
+
/** The folder a component lives in, which is not always its lowercased name. */
|
|
101
|
+
const folderFor = (component, beta = false) =>
|
|
102
|
+
(beta && CONFIG.betaFolders[component]) || component.toLowerCase();
|
|
103
|
+
|
|
70
104
|
const copyCommonFiles = async (destPath) => {
|
|
71
105
|
const commonFiles = [
|
|
72
106
|
{ src: ["..", "utils.ts"], dest: "utils.ts" },
|
|
@@ -83,18 +117,171 @@ const copyCommonFiles = async (destPath) => {
|
|
|
83
117
|
}
|
|
84
118
|
};
|
|
85
119
|
|
|
86
|
-
|
|
87
|
-
|
|
120
|
+
/* ------------------------------------------------------------ shared folder */
|
|
121
|
+
|
|
122
|
+
const sha256 = (filePath) =>
|
|
123
|
+
crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
124
|
+
|
|
125
|
+
/** Every file under a directory, relative to it, with "/" separators. */
|
|
126
|
+
const listFiles = (dir, prefix = "") =>
|
|
127
|
+
fs
|
|
128
|
+
.readdirSync(path.join(dir, prefix), { withFileTypes: true })
|
|
129
|
+
.flatMap((entry) => {
|
|
130
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
131
|
+
if (entry.name === MANIFEST_FILE || entry.name === TESTS_DIR) return [];
|
|
132
|
+
return entry.isDirectory() ? listFiles(dir, relative) : [relative];
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/** -1, 0 or 1, comparing dotted numeric versions such as "1.2.0". */
|
|
136
|
+
const compareVersions = (a, b) => {
|
|
137
|
+
const left = String(a).split(".").map(Number);
|
|
138
|
+
const right = String(b).split(".").map(Number);
|
|
139
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
140
|
+
const diff = (left[i] || 0) - (right[i] || 0);
|
|
141
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const readJson = (filePath, fallback = null) => {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
149
|
+
} catch {
|
|
150
|
+
return fallback;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Files the manifest says we wrote, whose contents have since changed. */
|
|
155
|
+
const locallyEdited = (dest, manifest) =>
|
|
156
|
+
!manifest
|
|
157
|
+
? []
|
|
158
|
+
: Object.keys(manifest.files).filter((file) => {
|
|
159
|
+
const target = path.join(dest, file);
|
|
160
|
+
return fs.existsSync(target) && manifest.files[file] !== sha256(target);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Installs or updates `shared/`.
|
|
165
|
+
*
|
|
166
|
+
* Because components are copied into a project one at a time, a project can end
|
|
167
|
+
* up with components built against different versions of this folder. The rules
|
|
168
|
+
* are that `shared/` only ever moves forward, and that a file the user has
|
|
169
|
+
* edited since we wrote it is never replaced without `--force`.
|
|
170
|
+
*/
|
|
171
|
+
const installShared = async (destPath, { force = false } = {}) => {
|
|
172
|
+
const src = path.join(BETA_SOURCE_PATH, SHARED_DIR);
|
|
173
|
+
const dest = path.join(destPath, SHARED_DIR);
|
|
174
|
+
|
|
175
|
+
if (!fs.existsSync(src)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`The shared folder is missing from ${BETA_SOURCE_PATH}. Beta components cannot be installed without it.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const srcVersion =
|
|
182
|
+
(readJson(path.join(src, "version.json"), {}) || {}).version || "0.0.0";
|
|
183
|
+
const manifest = readJson(path.join(dest, MANIFEST_FILE));
|
|
184
|
+
const installedVersion = manifest ? manifest.version : null;
|
|
185
|
+
const edited = locallyEdited(dest, manifest);
|
|
186
|
+
|
|
187
|
+
if (installedVersion && !force) {
|
|
188
|
+
const order = compareVersions(installedVersion, srcVersion);
|
|
189
|
+
|
|
190
|
+
if (order > 0) {
|
|
191
|
+
console.error(
|
|
192
|
+
`\n✗ This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
|
|
193
|
+
);
|
|
194
|
+
console.error(
|
|
195
|
+
" Installing it would downgrade the folder and break the components already here.",
|
|
196
|
+
);
|
|
197
|
+
console.error(" Update the CLI, or re-run with --force to overwrite it anyway.");
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (order === 0) {
|
|
202
|
+
console.log(
|
|
203
|
+
edited.length > 0
|
|
204
|
+
? `✓ shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
|
|
205
|
+
: `✓ shared v${installedVersion} is already installed`,
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (edited.length > 0 && !force) {
|
|
212
|
+
console.error(
|
|
213
|
+
`\n✗ These files in component-lib/${SHARED_DIR} have local changes:`,
|
|
214
|
+
);
|
|
215
|
+
edited.forEach((file) => console.error(` - ${file}`));
|
|
216
|
+
console.error(
|
|
217
|
+
"\n Updating shared/ would replace them. Re-run with --force to do that, or move",
|
|
218
|
+
);
|
|
219
|
+
console.error(
|
|
220
|
+
" your changes into your own module first: components are yours to edit,",
|
|
221
|
+
);
|
|
222
|
+
console.error(" shared/ is replaced on every update.");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const files = listFiles(src);
|
|
227
|
+
for (const file of files) {
|
|
228
|
+
await fs.copy(path.join(src, file), path.join(dest, file), { overwrite: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Remove what an older version left behind, but only files we installed and
|
|
232
|
+
// the user has not since changed.
|
|
233
|
+
if (manifest) {
|
|
234
|
+
const removed = Object.keys(manifest.files).filter((file) => !files.includes(file));
|
|
235
|
+
for (const file of removed) {
|
|
236
|
+
const target = path.join(dest, file);
|
|
237
|
+
if (fs.existsSync(target) && (force || manifest.files[file] === sha256(target))) {
|
|
238
|
+
await fs.remove(target);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.writeJsonSync(
|
|
244
|
+
path.join(dest, MANIFEST_FILE),
|
|
245
|
+
{
|
|
246
|
+
version: srcVersion,
|
|
247
|
+
installedAt: new Date().toISOString(),
|
|
248
|
+
files: Object.fromEntries(
|
|
249
|
+
files.map((file) => [file, sha256(path.join(src, file))]),
|
|
250
|
+
),
|
|
251
|
+
},
|
|
252
|
+
{ spaces: 2 },
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
console.log(
|
|
256
|
+
installedVersion
|
|
257
|
+
? `✓ shared updated ${installedVersion} → ${srcVersion}`
|
|
258
|
+
: `✓ shared v${srcVersion} installed`,
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** The files a component needs beside it, which differ between the two sets. */
|
|
263
|
+
const copySupportFiles = async (destPath, beta, options) => {
|
|
264
|
+
if (beta) {
|
|
265
|
+
await installShared(destPath, options);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// The legacy components' common files. Beta components use none of them.
|
|
269
|
+
if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
|
|
270
|
+
await copyCommonFiles(destPath);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/* -------------------------------------------------------------- components */
|
|
275
|
+
|
|
276
|
+
const checkComponentExists = (component, destPath, beta = false) => {
|
|
277
|
+
const componentPath = path.join(destPath, folderFor(component, beta));
|
|
88
278
|
return fs.existsSync(componentPath);
|
|
89
279
|
};
|
|
90
280
|
|
|
91
281
|
const copyComponent = async (component, destPath, beta = false) => {
|
|
92
282
|
try {
|
|
93
|
-
const componentSrc = path.join(
|
|
94
|
-
|
|
95
|
-
component.toLowerCase(),
|
|
96
|
-
);
|
|
97
|
-
const componentDest = path.join(destPath, component.toLowerCase());
|
|
283
|
+
const componentSrc = path.join(getSourcePath(beta), folderFor(component, beta));
|
|
284
|
+
const componentDest = path.join(destPath, folderFor(component, beta));
|
|
98
285
|
|
|
99
286
|
if (!fs.existsSync(componentSrc)) {
|
|
100
287
|
throw new Error(
|
|
@@ -102,7 +289,10 @@ const copyComponent = async (component, destPath, beta = false) => {
|
|
|
102
289
|
);
|
|
103
290
|
}
|
|
104
291
|
|
|
105
|
-
await fs.copy(componentSrc, componentDest, {
|
|
292
|
+
await fs.copy(componentSrc, componentDest, {
|
|
293
|
+
overwrite: true,
|
|
294
|
+
filter: (source) => !isTestPath(path.relative(componentSrc, source)),
|
|
295
|
+
});
|
|
106
296
|
console.log(
|
|
107
297
|
`✓ Component ${component} installed successfully ${
|
|
108
298
|
component === "Grid"
|
|
@@ -129,7 +319,7 @@ const installComponentWithDependencies = async (
|
|
|
129
319
|
const pendingInstalls = beta
|
|
130
320
|
? Array.from(componentsToInstall)
|
|
131
321
|
: Array.from(componentsToInstall).filter(
|
|
132
|
-
(comp) => !checkComponentExists(comp, destPath),
|
|
322
|
+
(comp) => !checkComponentExists(comp, destPath, beta),
|
|
133
323
|
);
|
|
134
324
|
|
|
135
325
|
if (pendingInstalls.length === 0) {
|
|
@@ -172,7 +362,7 @@ const installMultipleComponents = async (
|
|
|
172
362
|
const pendingInstalls = beta
|
|
173
363
|
? Array.from(allComponentsToInstall)
|
|
174
364
|
: Array.from(allComponentsToInstall).filter(
|
|
175
|
-
(comp) => !checkComponentExists(comp, destPath),
|
|
365
|
+
(comp) => !checkComponentExists(comp, destPath, beta),
|
|
176
366
|
);
|
|
177
367
|
|
|
178
368
|
if (pendingInstalls.length === 0) {
|
|
@@ -249,11 +439,11 @@ const interactiveComponentSelector = async (beta = false) => {
|
|
|
249
439
|
|
|
250
440
|
const handleKeyPress = (key) => {
|
|
251
441
|
switch (key) {
|
|
252
|
-
case "
|
|
442
|
+
case "[A": // Up arrow
|
|
253
443
|
currentIndex = Math.max(0, currentIndex - 1);
|
|
254
444
|
renderMenu();
|
|
255
445
|
break;
|
|
256
|
-
case "
|
|
446
|
+
case "[B": // Down arrow
|
|
257
447
|
currentIndex = Math.min(
|
|
258
448
|
getAvailableComponents(beta).length - 1,
|
|
259
449
|
currentIndex + 1,
|
|
@@ -310,6 +500,11 @@ const validateComponents = (components, beta = false) => {
|
|
|
310
500
|
(comp) => !availableComponents.includes(comp),
|
|
311
501
|
);
|
|
312
502
|
if (invalidComponents.length > 0) {
|
|
503
|
+
if (invalidComponents.some((comp) => comp.toLowerCase() === SHARED_DIR)) {
|
|
504
|
+
console.error(
|
|
505
|
+
"`shared` is not a component: it is installed automatically with any beta component.",
|
|
506
|
+
);
|
|
507
|
+
}
|
|
313
508
|
console.error(`Invalid components: ${invalidComponents.join(", ")}`);
|
|
314
509
|
console.log(`\nAvailable ${beta ? "beta " : ""}components:`);
|
|
315
510
|
availableComponents.forEach((comp) => console.log(`- ${comp}`));
|
|
@@ -348,6 +543,11 @@ const main = async () => {
|
|
|
348
543
|
type: "boolean",
|
|
349
544
|
default: false,
|
|
350
545
|
})
|
|
546
|
+
.option("force", {
|
|
547
|
+
describe: "Replace files in shared/ that you have edited locally",
|
|
548
|
+
type: "boolean",
|
|
549
|
+
default: false,
|
|
550
|
+
})
|
|
351
551
|
.example("$0 -a Button", "Install a single component")
|
|
352
552
|
.example("$0 -a Button,Card,Modal", "Install multiple components")
|
|
353
553
|
.example("$0 -a DataGrid -beta", "Install the redesigned beta DataGrid")
|
|
@@ -365,7 +565,11 @@ const main = async () => {
|
|
|
365
565
|
: "";
|
|
366
566
|
console.log(`- ${comp}${deps}`);
|
|
367
567
|
});
|
|
368
|
-
if (
|
|
568
|
+
if (argv.beta) {
|
|
569
|
+
console.log(
|
|
570
|
+
`\nEvery beta component also installs component-lib/${SHARED_DIR}, which they all import.`,
|
|
571
|
+
);
|
|
572
|
+
} else {
|
|
369
573
|
console.log("\nRedesigned beta components (install with -beta):");
|
|
370
574
|
CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
|
|
371
575
|
}
|
|
@@ -386,10 +590,7 @@ const main = async () => {
|
|
|
386
590
|
const destPath = DEFAULT_DEST_PATH;
|
|
387
591
|
await fs.ensureDir(destPath);
|
|
388
592
|
|
|
389
|
-
|
|
390
|
-
if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
|
|
391
|
-
await copyCommonFiles(destPath);
|
|
392
|
-
}
|
|
593
|
+
await copySupportFiles(destPath, argv.beta, { force: argv.force });
|
|
393
594
|
|
|
394
595
|
// Install selected components
|
|
395
596
|
await installMultipleComponents(selectedComponents, destPath, argv.beta);
|
|
@@ -420,10 +621,7 @@ const main = async () => {
|
|
|
420
621
|
const destPath = DEFAULT_DEST_PATH;
|
|
421
622
|
await fs.ensureDir(destPath);
|
|
422
623
|
|
|
423
|
-
|
|
424
|
-
if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
|
|
425
|
-
await copyCommonFiles(destPath);
|
|
426
|
-
}
|
|
624
|
+
await copySupportFiles(destPath, argv.beta, { force: argv.force });
|
|
427
625
|
|
|
428
626
|
// Install components
|
|
429
627
|
if (components.length === 1) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Breadcrumb
|
|
2
|
+
|
|
3
|
+
A breadcrumb trail for React 19, styled to match the rest of the library. No
|
|
4
|
+
runtime dependencies besides React.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { Breadcrumb } from "@/components/breadcrumb";
|
|
8
|
+
import "@/components/breadcrumb/styles.css";
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<Breadcrumb
|
|
13
|
+
items={[
|
|
14
|
+
{ label: "Home", href: "/" },
|
|
15
|
+
{ label: "Invoices", href: "/invoices" },
|
|
16
|
+
{ label: "INV-2026-0042" },
|
|
17
|
+
]}
|
|
18
|
+
maxItems={4}
|
|
19
|
+
renderLink={(props) => <Link {...props} />}
|
|
20
|
+
structuredData={{ baseUrl: "https://app.example.com" }}
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Prop | Default | Description |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `items` | required | `{ label, href?, icon?, name?, onClick? }`, top level first; the last is the current page. |
|
|
27
|
+
| `separator` | chevron | Between crumbs. |
|
|
28
|
+
| `maxItems`, `itemsBeforeCollapse`, `itemsAfterCollapse` | —, `1`, `1` | Collapse the middle of long trails behind an ellipsis. |
|
|
29
|
+
| `renderLink` | `<a>` | Draw links with your router. |
|
|
30
|
+
| `structuredData` | `false` | Adds schema.org `BreadcrumbList` JSON-LD; pass `{ baseUrl }`. |
|
|
31
|
+
| `size`, `className`, `classNames`, `localeText` | `md` | Slots: `root` `list` `item` `link` `current` `separator` `ellipsis`. |
|
|
32
|
+
|
|
33
|
+
`collapseItems` and `toBreadcrumbJsonLd` are exported from the framework-free
|
|
34
|
+
`core`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { collapseItems, toBreadcrumbJsonLd } from "../core/trail";
|
|
3
|
+
|
|
4
|
+
const shape = (slots: ReturnType<typeof collapseItems>) =>
|
|
5
|
+
slots.map((slot) => (slot.kind === "item" ? slot.index : `…${slot.hidden.join(",")}`));
|
|
6
|
+
|
|
7
|
+
describe("collapsing", () => {
|
|
8
|
+
it("leaves a short trail alone", () => {
|
|
9
|
+
expect(shape(collapseItems(3))).toEqual([0, 1, 2]);
|
|
10
|
+
expect(shape(collapseItems(4, 4))).toEqual([0, 1, 2, 3]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("keeps the first and last crumbs around an ellipsis", () => {
|
|
14
|
+
expect(shape(collapseItems(6, 4))).toEqual([0, "…1,2,3,4", 5]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("keeps more on either side when asked", () => {
|
|
18
|
+
expect(shape(collapseItems(7, 4, 1, 2))).toEqual([0, "…1,2,3,4", 5, 6]);
|
|
19
|
+
expect(shape(collapseItems(7, 4, 2, 2))).toEqual([0, 1, "…2,3,4", 5, 6]);
|
|
20
|
+
expect(shape(collapseItems(7, 4, 0, 1))).toEqual(["…0,1,2,3,4,5", 6]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("doesn't collapse when the kept crumbs already cover the trail", () => {
|
|
24
|
+
expect(shape(collapseItems(4, 2, 2, 2))).toEqual([0, 1, 2, 3]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("always keeps the current page", () => {
|
|
28
|
+
expect(shape(collapseItems(5, 2, 1, 0))).toEqual([0, "…1,2,3", 4]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("structured data", () => {
|
|
33
|
+
it("builds a schema.org BreadcrumbList with absolute links", () => {
|
|
34
|
+
const data = toBreadcrumbJsonLd(
|
|
35
|
+
[
|
|
36
|
+
{ name: "Home", href: "/" },
|
|
37
|
+
{ name: "Invoices", href: "/invoices" },
|
|
38
|
+
{ name: "INV-042" },
|
|
39
|
+
],
|
|
40
|
+
"https://app.example.com/billing/",
|
|
41
|
+
);
|
|
42
|
+
expect(data).toEqual({
|
|
43
|
+
"@context": "https://schema.org",
|
|
44
|
+
"@type": "BreadcrumbList",
|
|
45
|
+
itemListElement: [
|
|
46
|
+
{ "@type": "ListItem", position: 1, name: "Home", item: "https://app.example.com/" },
|
|
47
|
+
{ "@type": "ListItem", position: 2, name: "Invoices", item: "https://app.example.com/invoices" },
|
|
48
|
+
{ "@type": "ListItem", position: 3, name: "INV-042" },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("keeps links as given without a base URL", () => {
|
|
54
|
+
expect(toBreadcrumbJsonLd([{ name: "Docs", href: "https://example.com/docs" }]).itemListElement[0].item).toBe(
|
|
55
|
+
"https://example.com/docs",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CrumbSlot } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collapses a long trail to its first and last crumbs with an ellipsis between,
|
|
5
|
+
* once it has more than `maxItems`. Returns the trail unchanged when it fits.
|
|
6
|
+
*/
|
|
7
|
+
export function collapseItems(
|
|
8
|
+
count: number,
|
|
9
|
+
maxItems?: number,
|
|
10
|
+
itemsBefore = 1,
|
|
11
|
+
itemsAfter = 1,
|
|
12
|
+
): CrumbSlot[] {
|
|
13
|
+
const before = Math.max(0, itemsBefore);
|
|
14
|
+
const after = Math.max(1, itemsAfter);
|
|
15
|
+
const all: CrumbSlot[] = Array.from({ length: count }, (_, index) => ({ kind: "item", index }));
|
|
16
|
+
if (maxItems === undefined || count <= maxItems || before + after >= count) return all;
|
|
17
|
+
|
|
18
|
+
const hidden = Array.from({ length: count - before - after }, (_, offset) => before + offset);
|
|
19
|
+
return [...all.slice(0, before), { kind: "ellipsis", hidden }, ...all.slice(count - after)];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface JsonLdCrumb {
|
|
23
|
+
name: string;
|
|
24
|
+
href?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* schema.org `BreadcrumbList` data, which search engines use to show the trail
|
|
29
|
+
* in results. Relative links are resolved against `baseUrl`.
|
|
30
|
+
*/
|
|
31
|
+
export function toBreadcrumbJsonLd(items: readonly JsonLdCrumb[], baseUrl?: string) {
|
|
32
|
+
return {
|
|
33
|
+
"@context": "https://schema.org",
|
|
34
|
+
"@type": "BreadcrumbList",
|
|
35
|
+
itemListElement: items.map((item, index) => ({
|
|
36
|
+
"@type": "ListItem",
|
|
37
|
+
position: index + 1,
|
|
38
|
+
name: item.name,
|
|
39
|
+
...(item.href ? { item: baseUrl ? new URL(item.href, baseUrl).href : item.href } : {}),
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface BreadcrumbItem {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
/** Omit for the current page or a crumb that isn't a link. */
|
|
6
|
+
href?: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
/** Plain-text name, for structured data when `label` isn't a string. */
|
|
9
|
+
name?: string;
|
|
10
|
+
/** Called on click, e.g. for client-side navigation without an href. */
|
|
11
|
+
onClick?(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One position in the rendered trail. */
|
|
15
|
+
export type CrumbSlot = { kind: "item"; index: number } | { kind: "ellipsis"; hidden: number[] };
|
|
16
|
+
|
|
17
|
+
export interface BreadcrumbLocaleText {
|
|
18
|
+
label: string;
|
|
19
|
+
showMore(count: string): string;
|
|
20
|
+
}
|