gbs-add-block 1.2.15 → 1.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +219 -21
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/react/props.ts +2 -2
  5. package/source/beta-components/breadcrumb/styles.css +8 -8
  6. package/source/beta-components/button/react/props.ts +2 -2
  7. package/source/beta-components/button/styles.css +15 -15
  8. package/source/beta-components/card/README.md +60 -0
  9. package/source/beta-components/card/__tests__/core.test.ts +57 -0
  10. package/source/beta-components/card/core/index.ts +4 -0
  11. package/source/beta-components/card/core/trend.ts +32 -0
  12. package/source/beta-components/card/core/types.ts +5 -0
  13. package/source/beta-components/card/index.ts +5 -0
  14. package/source/beta-components/card/react/Card.tsx +133 -0
  15. package/source/beta-components/card/react/Stat.tsx +127 -0
  16. package/source/beta-components/card/react/props.ts +4 -0
  17. package/source/beta-components/card/styles.css +231 -0
  18. package/source/beta-components/checkbox/react/Checkbox.tsx +147 -146
  19. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +8 -8
  20. package/source/beta-components/checkbox/react/props.ts +2 -2
  21. package/source/beta-components/checkbox/styles.css +11 -11
  22. package/source/beta-components/combobox/README.md +3 -3
  23. package/source/beta-components/combobox/react/MultiSelect.tsx +265 -263
  24. package/source/beta-components/combobox/react/Popover.tsx +38 -70
  25. package/source/beta-components/combobox/react/Select.tsx +200 -198
  26. package/source/beta-components/combobox/react/icons.tsx +6 -29
  27. package/source/beta-components/combobox/react/props.ts +4 -2
  28. package/source/beta-components/combobox/styles.css +18 -18
  29. package/source/beta-components/data-grid/react/Popover.tsx +35 -0
  30. package/source/beta-components/{datagrid → data-grid}/react/context.ts +2 -2
  31. package/source/beta-components/{datagrid → data-grid}/react/icons.tsx +8 -35
  32. package/source/beta-components/{datagrid → data-grid}/styles.css +24 -24
  33. package/source/beta-components/{datepicker → date-picker}/README.md +3 -3
  34. package/source/beta-components/{datepicker → date-picker}/react/DatePicker.tsx +249 -249
  35. package/source/beta-components/{datepicker → date-picker}/react/DateRangePicker.tsx +291 -291
  36. package/source/beta-components/date-picker/react/Popover.tsx +45 -0
  37. package/source/beta-components/date-picker/react/icons.tsx +15 -0
  38. package/source/beta-components/{datepicker → date-picker}/react/props.ts +2 -2
  39. package/source/beta-components/{datepicker → date-picker}/styles.css +18 -18
  40. package/source/beta-components/dialog/react/icons.tsx +16 -47
  41. package/source/beta-components/dialog/react/props.ts +2 -2
  42. package/source/beta-components/dialog/styles.css +21 -21
  43. package/source/beta-components/{fileuploader → file-uploader}/react/FileUploader.tsx +511 -515
  44. package/source/beta-components/{fileuploader → file-uploader}/react/icons.tsx +7 -27
  45. package/source/beta-components/{fileuploader → file-uploader}/react/props.ts +2 -2
  46. package/source/beta-components/{fileuploader → file-uploader}/styles.css +17 -17
  47. package/source/beta-components/input/core/count.ts +3 -12
  48. package/source/beta-components/input/react/Input.tsx +219 -219
  49. package/source/beta-components/input/react/OtpInput.tsx +256 -256
  50. package/source/beta-components/input/react/icons.tsx +3 -21
  51. package/source/beta-components/input/react/props.ts +2 -2
  52. package/source/beta-components/input/styles.css +15 -15
  53. package/source/beta-components/menu/README.md +68 -0
  54. package/source/beta-components/menu/__tests__/core.test.ts +120 -0
  55. package/source/beta-components/menu/core/index.ts +11 -0
  56. package/source/beta-components/menu/core/navigation.ts +84 -0
  57. package/source/beta-components/menu/core/types.ts +32 -0
  58. package/source/beta-components/menu/index.ts +19 -0
  59. package/source/beta-components/menu/react/Menu.tsx +371 -0
  60. package/source/beta-components/menu/react/context.ts +31 -0
  61. package/source/beta-components/menu/react/items.tsx +268 -0
  62. package/source/beta-components/menu/react/locale.ts +6 -0
  63. package/source/beta-components/menu/react/props.ts +4 -0
  64. package/source/beta-components/menu/react/useMenuKeys.ts +124 -0
  65. package/source/beta-components/menu/styles.css +222 -0
  66. package/source/beta-components/modal/react/Modal.tsx +242 -238
  67. package/source/beta-components/modal/react/icons.tsx +1 -19
  68. package/source/beta-components/modal/react/props.ts +2 -2
  69. package/source/beta-components/modal/styles.css +17 -13
  70. package/source/beta-components/popover/README.md +44 -0
  71. package/source/beta-components/popover/core/index.ts +2 -0
  72. package/source/beta-components/popover/core/types.ts +12 -0
  73. package/source/beta-components/popover/index.ts +4 -0
  74. package/source/beta-components/popover/react/Popover.tsx +202 -0
  75. package/source/beta-components/popover/react/locale.ts +5 -0
  76. package/source/beta-components/popover/react/props.ts +4 -0
  77. package/source/beta-components/popover/styles.css +146 -0
  78. package/source/beta-components/shared/README.md +54 -0
  79. package/source/beta-components/shared/__tests__/boundaries.test.ts +95 -0
  80. package/source/beta-components/shared/__tests__/core.test.ts +55 -0
  81. package/source/beta-components/shared/__tests__/position.test.ts +143 -0
  82. package/source/beta-components/shared/core/cx.ts +9 -0
  83. package/source/beta-components/shared/core/field.ts +35 -0
  84. package/source/beta-components/shared/core/position.ts +124 -0
  85. package/source/beta-components/shared/core/text.ts +14 -0
  86. package/source/beta-components/shared/index.ts +8 -0
  87. package/source/beta-components/shared/react/Popover.tsx +189 -0
  88. package/source/beta-components/shared/react/icons.tsx +74 -0
  89. package/source/beta-components/shared/react/useControllableState.ts +42 -0
  90. package/source/beta-components/shared/version.json +4 -0
  91. package/source/beta-components/shared/version.ts +6 -0
  92. package/source/beta-components/skeleton/README.md +58 -0
  93. package/source/beta-components/skeleton/__tests__/core.test.ts +41 -0
  94. package/source/beta-components/skeleton/core/index.ts +3 -0
  95. package/source/beta-components/skeleton/core/lines.ts +25 -0
  96. package/source/beta-components/skeleton/core/types.ts +5 -0
  97. package/source/beta-components/skeleton/index.ts +5 -0
  98. package/source/beta-components/skeleton/react/Empty.tsx +51 -0
  99. package/source/beta-components/skeleton/react/Skeleton.tsx +92 -0
  100. package/source/beta-components/skeleton/react/props.ts +4 -0
  101. package/source/beta-components/skeleton/styles.css +186 -0
  102. package/source/beta-components/spinner/react/props.ts +2 -2
  103. package/source/beta-components/spinner/styles.css +174 -167
  104. package/source/beta-components/tabs/react/Tabs.tsx +306 -260
  105. package/source/beta-components/tabs/styles.css +264 -257
  106. package/source/beta-components/textarea/core/count.ts +3 -12
  107. package/source/beta-components/textarea/react/Textarea.tsx +208 -209
  108. package/source/beta-components/textarea/react/props.ts +2 -2
  109. package/source/beta-components/textarea/styles.css +12 -12
  110. package/source/beta-components/toaster/react/icons.tsx +19 -52
  111. package/source/beta-components/toaster/react/props.ts +2 -2
  112. package/source/beta-components/toaster/styles.css +18 -18
  113. package/source/beta-components/tooltip/README.md +43 -0
  114. package/source/beta-components/tooltip/__tests__/core.test.ts +42 -0
  115. package/source/beta-components/tooltip/core/index.ts +4 -0
  116. package/source/beta-components/tooltip/core/types.ts +4 -0
  117. package/source/beta-components/tooltip/core/visibility.ts +34 -0
  118. package/source/beta-components/tooltip/index.ts +3 -0
  119. package/source/beta-components/tooltip/react/Tooltip.tsx +196 -0
  120. package/source/beta-components/tooltip/react/props.ts +4 -0
  121. package/source/beta-components/tooltip/styles.css +72 -0
  122. package/source/beta-components/datagrid/react/Popover.tsx +0 -79
  123. package/source/beta-components/datepicker/react/Popover.tsx +0 -87
  124. package/source/beta-components/datepicker/react/icons.tsx +0 -32
  125. /package/source/beta-components/{datagrid → data-grid}/README.md +0 -0
  126. /package/source/beta-components/{datagrid → data-grid}/__tests__/core.test.ts +0 -0
  127. /package/source/beta-components/{datagrid → data-grid}/__tests__/export.test.ts +0 -0
  128. /package/source/beta-components/{datagrid → data-grid}/core/columnHelper.ts +0 -0
  129. /package/source/beta-components/{datagrid → data-grid}/core/columns.ts +0 -0
  130. /package/source/beta-components/{datagrid → data-grid}/core/filtering.ts +0 -0
  131. /package/source/beta-components/{datagrid → data-grid}/core/grid.ts +0 -0
  132. /package/source/beta-components/{datagrid → data-grid}/core/index.ts +0 -0
  133. /package/source/beta-components/{datagrid → data-grid}/core/rows.ts +0 -0
  134. /package/source/beta-components/{datagrid → data-grid}/core/sorting.ts +0 -0
  135. /package/source/beta-components/{datagrid → data-grid}/core/state.ts +0 -0
  136. /package/source/beta-components/{datagrid → data-grid}/core/store.ts +0 -0
  137. /package/source/beta-components/{datagrid → data-grid}/core/types.ts +0 -0
  138. /package/source/beta-components/{datagrid → data-grid}/core/values.ts +0 -0
  139. /package/source/beta-components/{datagrid → data-grid}/core/virtual.ts +0 -0
  140. /package/source/beta-components/{datagrid → data-grid}/export/csv.ts +0 -0
  141. /package/source/beta-components/{datagrid → data-grid}/export/download.ts +0 -0
  142. /package/source/beta-components/{datagrid → data-grid}/export/pdf.ts +0 -0
  143. /package/source/beta-components/{datagrid → data-grid}/export/table.ts +0 -0
  144. /package/source/beta-components/{datagrid → data-grid}/export/xlsx.ts +0 -0
  145. /package/source/beta-components/{datagrid → data-grid}/export/zip.ts +0 -0
  146. /package/source/beta-components/{datagrid → data-grid}/index.ts +0 -0
  147. /package/source/beta-components/{datagrid → data-grid}/react/Cell.tsx +0 -0
  148. /package/source/beta-components/{datagrid → data-grid}/react/ColumnMenu.tsx +0 -0
  149. /package/source/beta-components/{datagrid → data-grid}/react/DataGrid.tsx +0 -0
  150. /package/source/beta-components/{datagrid → data-grid}/react/FilterForm.tsx +0 -0
  151. /package/source/beta-components/{datagrid → data-grid}/react/HeaderRow.tsx +0 -0
  152. /package/source/beta-components/{datagrid → data-grid}/react/Pagination.tsx +0 -0
  153. /package/source/beta-components/{datagrid → data-grid}/react/Row.tsx +0 -0
  154. /package/source/beta-components/{datagrid → data-grid}/react/Toolbar.tsx +0 -0
  155. /package/source/beta-components/{datagrid → data-grid}/react/Viewport.tsx +0 -0
  156. /package/source/beta-components/{datagrid → data-grid}/react/hooks.ts +0 -0
  157. /package/source/beta-components/{datagrid → data-grid}/react/keyboard.ts +0 -0
  158. /package/source/beta-components/{datagrid → data-grid}/react/locale.ts +0 -0
  159. /package/source/beta-components/{datepicker → date-picker}/__tests__/core.test.ts +0 -0
  160. /package/source/beta-components/{datepicker → date-picker}/core/calendar.ts +0 -0
  161. /package/source/beta-components/{datepicker → date-picker}/core/format.ts +0 -0
  162. /package/source/beta-components/{datepicker → date-picker}/core/index.ts +0 -0
  163. /package/source/beta-components/{datepicker → date-picker}/core/types.ts +0 -0
  164. /package/source/beta-components/{datepicker → date-picker}/index.ts +0 -0
  165. /package/source/beta-components/{datepicker → date-picker}/react/Calendar.tsx +0 -0
  166. /package/source/beta-components/{datepicker → date-picker}/react/CalendarPanel.tsx +0 -0
  167. /package/source/beta-components/{datepicker → date-picker}/react/MonthYearPanels.tsx +0 -0
  168. /package/source/beta-components/{datepicker → date-picker}/react/locale.ts +0 -0
  169. /package/source/beta-components/{datepicker → date-picker}/react/useDatePicker.ts +0 -0
  170. /package/source/beta-components/{fileuploader → file-uploader}/README.md +0 -0
  171. /package/source/beta-components/{fileuploader → file-uploader}/__tests__/core.test.ts +0 -0
  172. /package/source/beta-components/{fileuploader → file-uploader}/core/format.ts +0 -0
  173. /package/source/beta-components/{fileuploader → file-uploader}/core/index.ts +0 -0
  174. /package/source/beta-components/{fileuploader → file-uploader}/core/store.ts +0 -0
  175. /package/source/beta-components/{fileuploader → file-uploader}/core/transport.ts +0 -0
  176. /package/source/beta-components/{fileuploader → file-uploader}/core/types.ts +0 -0
  177. /package/source/beta-components/{fileuploader → file-uploader}/core/validate.ts +0 -0
  178. /package/source/beta-components/{fileuploader → file-uploader}/index.ts +0 -0
  179. /package/source/beta-components/{fileuploader → file-uploader}/react/FileRow.tsx +0 -0
  180. /package/source/beta-components/{fileuploader → file-uploader}/react/locale.ts +0 -0
  181. /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.15)
1
+ # GBS Building Blocks 2.0 (v1.2.17)
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.15)
13
+ ## What's New 🎉 (Ver 1.2.17)
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");
@@ -50,7 +51,18 @@ const CONFIG = {
50
51
  "Checkbox",
51
52
  "Tabs",
52
53
  "Spinner",
54
+ "Menu",
55
+ "Tooltip",
56
+ "Popover",
57
+ "Card",
58
+ "Skeleton",
53
59
  ],
60
+ // Beta folder names that are not simply the lowercased component name.
61
+ betaFolders: {
62
+ DataGrid: "data-grid",
63
+ DatePicker: "date-picker",
64
+ FileUploader: "file-uploader",
65
+ },
54
66
  // Define component dependencies
55
67
  dependencies: {
56
68
  FormRenderer: ["Select", "MultiSelect", "Input", "DatePicker"],
@@ -62,6 +74,24 @@ const SOURCE_PATH = path.join(__dirname, "source", "components");
62
74
  const BETA_SOURCE_PATH = path.join(__dirname, "source", "beta-components");
63
75
  const DEFAULT_DEST_PATH = path.join(process.cwd(), "component-lib");
64
76
 
77
+ /**
78
+ * Every beta component imports its shared helpers from `../../shared`, so this
79
+ * folder is installed alongside whatever the user picked. It is not a component
80
+ * and never appears in the component list.
81
+ */
82
+ const SHARED_DIR = "shared";
83
+ const MANIFEST_FILE = ".install-manifest.json";
84
+
85
+ /**
86
+ * Tests belong to this repo, not to the projects we install into: they import
87
+ * vitest, which the project has no reason to have, and the shared folder's
88
+ * boundary tests would run against whatever else is in the user's
89
+ * component-lib.
90
+ */
91
+ const TESTS_DIR = "__tests__";
92
+ const isTestPath = (filePath) =>
93
+ filePath.split(/[\\/]/).includes(TESTS_DIR);
94
+
65
95
  const normalizeComponent = (component, availableComponents) =>
66
96
  availableComponents.find(
67
97
  (available) => available.toLowerCase() === component.toLowerCase(),
@@ -72,6 +102,10 @@ const getAvailableComponents = (beta = false) =>
72
102
 
73
103
  const getSourcePath = (beta = false) => (beta ? BETA_SOURCE_PATH : SOURCE_PATH);
74
104
 
105
+ /** The folder a component lives in, which is not always its lowercased name. */
106
+ const folderFor = (component, beta = false) =>
107
+ (beta && CONFIG.betaFolders[component]) || component.toLowerCase();
108
+
75
109
  const copyCommonFiles = async (destPath) => {
76
110
  const commonFiles = [
77
111
  { src: ["..", "utils.ts"], dest: "utils.ts" },
@@ -88,18 +122,171 @@ const copyCommonFiles = async (destPath) => {
88
122
  }
89
123
  };
90
124
 
91
- const checkComponentExists = (component, destPath) => {
92
- const componentPath = path.join(destPath, component.toLowerCase());
125
+ /* ------------------------------------------------------------ shared folder */
126
+
127
+ const sha256 = (filePath) =>
128
+ crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
129
+
130
+ /** Every file under a directory, relative to it, with "/" separators. */
131
+ const listFiles = (dir, prefix = "") =>
132
+ fs
133
+ .readdirSync(path.join(dir, prefix), { withFileTypes: true })
134
+ .flatMap((entry) => {
135
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
136
+ if (entry.name === MANIFEST_FILE || entry.name === TESTS_DIR) return [];
137
+ return entry.isDirectory() ? listFiles(dir, relative) : [relative];
138
+ });
139
+
140
+ /** -1, 0 or 1, comparing dotted numeric versions such as "1.2.0". */
141
+ const compareVersions = (a, b) => {
142
+ const left = String(a).split(".").map(Number);
143
+ const right = String(b).split(".").map(Number);
144
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
145
+ const diff = (left[i] || 0) - (right[i] || 0);
146
+ if (diff !== 0) return diff > 0 ? 1 : -1;
147
+ }
148
+ return 0;
149
+ };
150
+
151
+ const readJson = (filePath, fallback = null) => {
152
+ try {
153
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
154
+ } catch {
155
+ return fallback;
156
+ }
157
+ };
158
+
159
+ /** Files the manifest says we wrote, whose contents have since changed. */
160
+ const locallyEdited = (dest, manifest) =>
161
+ !manifest
162
+ ? []
163
+ : Object.keys(manifest.files).filter((file) => {
164
+ const target = path.join(dest, file);
165
+ return fs.existsSync(target) && manifest.files[file] !== sha256(target);
166
+ });
167
+
168
+ /**
169
+ * Installs or updates `shared/`.
170
+ *
171
+ * Because components are copied into a project one at a time, a project can end
172
+ * up with components built against different versions of this folder. The rules
173
+ * are that `shared/` only ever moves forward, and that a file the user has
174
+ * edited since we wrote it is never replaced without `--force`.
175
+ */
176
+ const installShared = async (destPath, { force = false } = {}) => {
177
+ const src = path.join(BETA_SOURCE_PATH, SHARED_DIR);
178
+ const dest = path.join(destPath, SHARED_DIR);
179
+
180
+ if (!fs.existsSync(src)) {
181
+ throw new Error(
182
+ `The shared folder is missing from ${BETA_SOURCE_PATH}. Beta components cannot be installed without it.`,
183
+ );
184
+ }
185
+
186
+ const srcVersion =
187
+ (readJson(path.join(src, "version.json"), {}) || {}).version || "0.0.0";
188
+ const manifest = readJson(path.join(dest, MANIFEST_FILE));
189
+ const installedVersion = manifest ? manifest.version : null;
190
+ const edited = locallyEdited(dest, manifest);
191
+
192
+ if (installedVersion && !force) {
193
+ const order = compareVersions(installedVersion, srcVersion);
194
+
195
+ if (order > 0) {
196
+ console.error(
197
+ `\n✗ This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
198
+ );
199
+ console.error(
200
+ " Installing it would downgrade the folder and break the components already here.",
201
+ );
202
+ console.error(" Update the CLI, or re-run with --force to overwrite it anyway.");
203
+ process.exit(1);
204
+ }
205
+
206
+ if (order === 0) {
207
+ console.log(
208
+ edited.length > 0
209
+ ? `✓ shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
210
+ : `✓ shared v${installedVersion} is already installed`,
211
+ );
212
+ return;
213
+ }
214
+ }
215
+
216
+ if (edited.length > 0 && !force) {
217
+ console.error(
218
+ `\n✗ These files in component-lib/${SHARED_DIR} have local changes:`,
219
+ );
220
+ edited.forEach((file) => console.error(` - ${file}`));
221
+ console.error(
222
+ "\n Updating shared/ would replace them. Re-run with --force to do that, or move",
223
+ );
224
+ console.error(
225
+ " your changes into your own module first: components are yours to edit,",
226
+ );
227
+ console.error(" shared/ is replaced on every update.");
228
+ process.exit(1);
229
+ }
230
+
231
+ const files = listFiles(src);
232
+ for (const file of files) {
233
+ await fs.copy(path.join(src, file), path.join(dest, file), { overwrite: true });
234
+ }
235
+
236
+ // Remove what an older version left behind, but only files we installed and
237
+ // the user has not since changed.
238
+ if (manifest) {
239
+ const removed = Object.keys(manifest.files).filter((file) => !files.includes(file));
240
+ for (const file of removed) {
241
+ const target = path.join(dest, file);
242
+ if (fs.existsSync(target) && (force || manifest.files[file] === sha256(target))) {
243
+ await fs.remove(target);
244
+ }
245
+ }
246
+ }
247
+
248
+ fs.writeJsonSync(
249
+ path.join(dest, MANIFEST_FILE),
250
+ {
251
+ version: srcVersion,
252
+ installedAt: new Date().toISOString(),
253
+ files: Object.fromEntries(
254
+ files.map((file) => [file, sha256(path.join(src, file))]),
255
+ ),
256
+ },
257
+ { spaces: 2 },
258
+ );
259
+
260
+ console.log(
261
+ installedVersion
262
+ ? `✓ shared updated ${installedVersion} → ${srcVersion}`
263
+ : `✓ shared v${srcVersion} installed`,
264
+ );
265
+ };
266
+
267
+ /** The files a component needs beside it, which differ between the two sets. */
268
+ const copySupportFiles = async (destPath, beta, options) => {
269
+ if (beta) {
270
+ await installShared(destPath, options);
271
+ return;
272
+ }
273
+ // The legacy components' common files. Beta components use none of them.
274
+ if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
275
+ await copyCommonFiles(destPath);
276
+ }
277
+ };
278
+
279
+ /* -------------------------------------------------------------- components */
280
+
281
+ const checkComponentExists = (component, destPath, beta = false) => {
282
+ const componentPath = path.join(destPath, folderFor(component, beta));
93
283
  return fs.existsSync(componentPath);
94
284
  };
95
285
 
96
286
  const copyComponent = async (component, destPath, beta = false) => {
97
287
  try {
98
- const componentSrc = path.join(
99
- getSourcePath(beta),
100
- component.toLowerCase(),
101
- );
102
- const componentDest = path.join(destPath, component.toLowerCase());
288
+ const componentSrc = path.join(getSourcePath(beta), folderFor(component, beta));
289
+ const componentDest = path.join(destPath, folderFor(component, beta));
103
290
 
104
291
  if (!fs.existsSync(componentSrc)) {
105
292
  throw new Error(
@@ -107,7 +294,10 @@ const copyComponent = async (component, destPath, beta = false) => {
107
294
  );
108
295
  }
109
296
 
110
- await fs.copy(componentSrc, componentDest, { overwrite: true });
297
+ await fs.copy(componentSrc, componentDest, {
298
+ overwrite: true,
299
+ filter: (source) => !isTestPath(path.relative(componentSrc, source)),
300
+ });
111
301
  console.log(
112
302
  `✓ Component ${component} installed successfully ${
113
303
  component === "Grid"
@@ -134,7 +324,7 @@ const installComponentWithDependencies = async (
134
324
  const pendingInstalls = beta
135
325
  ? Array.from(componentsToInstall)
136
326
  : Array.from(componentsToInstall).filter(
137
- (comp) => !checkComponentExists(comp, destPath),
327
+ (comp) => !checkComponentExists(comp, destPath, beta),
138
328
  );
139
329
 
140
330
  if (pendingInstalls.length === 0) {
@@ -177,7 +367,7 @@ const installMultipleComponents = async (
177
367
  const pendingInstalls = beta
178
368
  ? Array.from(allComponentsToInstall)
179
369
  : Array.from(allComponentsToInstall).filter(
180
- (comp) => !checkComponentExists(comp, destPath),
370
+ (comp) => !checkComponentExists(comp, destPath, beta),
181
371
  );
182
372
 
183
373
  if (pendingInstalls.length === 0) {
@@ -254,11 +444,11 @@ const interactiveComponentSelector = async (beta = false) => {
254
444
 
255
445
  const handleKeyPress = (key) => {
256
446
  switch (key) {
257
- case "\u001b[A": // Up arrow
447
+ case "": // Up arrow
258
448
  currentIndex = Math.max(0, currentIndex - 1);
259
449
  renderMenu();
260
450
  break;
261
- case "\u001b[B": // Down arrow
451
+ case "": // Down arrow
262
452
  currentIndex = Math.min(
263
453
  getAvailableComponents(beta).length - 1,
264
454
  currentIndex + 1,
@@ -315,6 +505,11 @@ const validateComponents = (components, beta = false) => {
315
505
  (comp) => !availableComponents.includes(comp),
316
506
  );
317
507
  if (invalidComponents.length > 0) {
508
+ if (invalidComponents.some((comp) => comp.toLowerCase() === SHARED_DIR)) {
509
+ console.error(
510
+ "`shared` is not a component: it is installed automatically with any beta component.",
511
+ );
512
+ }
318
513
  console.error(`Invalid components: ${invalidComponents.join(", ")}`);
319
514
  console.log(`\nAvailable ${beta ? "beta " : ""}components:`);
320
515
  availableComponents.forEach((comp) => console.log(`- ${comp}`));
@@ -353,6 +548,11 @@ const main = async () => {
353
548
  type: "boolean",
354
549
  default: false,
355
550
  })
551
+ .option("force", {
552
+ describe: "Replace files in shared/ that you have edited locally",
553
+ type: "boolean",
554
+ default: false,
555
+ })
356
556
  .example("$0 -a Button", "Install a single component")
357
557
  .example("$0 -a Button,Card,Modal", "Install multiple components")
358
558
  .example("$0 -a DataGrid -beta", "Install the redesigned beta DataGrid")
@@ -370,7 +570,11 @@ const main = async () => {
370
570
  : "";
371
571
  console.log(`- ${comp}${deps}`);
372
572
  });
373
- if (!argv.beta) {
573
+ if (argv.beta) {
574
+ console.log(
575
+ `\nEvery beta component also installs component-lib/${SHARED_DIR}, which they all import.`,
576
+ );
577
+ } else {
374
578
  console.log("\nRedesigned beta components (install with -beta):");
375
579
  CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
376
580
  }
@@ -391,10 +595,7 @@ const main = async () => {
391
595
  const destPath = DEFAULT_DEST_PATH;
392
596
  await fs.ensureDir(destPath);
393
597
 
394
- // Copy common files if they don't exist
395
- if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
396
- await copyCommonFiles(destPath);
397
- }
598
+ await copySupportFiles(destPath, argv.beta, { force: argv.force });
398
599
 
399
600
  // Install selected components
400
601
  await installMultipleComponents(selectedComponents, destPath, argv.beta);
@@ -425,10 +626,7 @@ const main = async () => {
425
626
  const destPath = DEFAULT_DEST_PATH;
426
627
  await fs.ensureDir(destPath);
427
628
 
428
- // Copy common files if they don't exist
429
- if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
430
- await copyCommonFiles(destPath);
431
- }
629
+ await copySupportFiles(destPath, argv.beta, { force: argv.force });
432
630
 
433
631
  // Install components
434
632
  if (components.length === 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbs-add-block",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "description": "React Component Library",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,5 +10,5 @@ export interface BreadcrumbLinkProps {
10
10
  onClick?: MouseEventHandler<HTMLAnchorElement>;
11
11
  }
12
12
 
13
- export const cx = (...names: (string | false | null | undefined)[]) =>
14
- names.filter(Boolean).join(" ");
13
+ // Shared with every other component, so class handling cannot drift.
14
+ export { cx } from "../../shared/core/cx";
@@ -1,20 +1,20 @@
1
1
  /*
2
2
  * Breadcrumb styles.
3
3
  *
4
- * Tokens read the DataGrid's --dg-* variables when they are set on an ancestor
5
- * (set them on :root to share one theme) and fall back to the same palette
6
- * otherwise. Rules live in the `components` layer, so utility classes passed
4
+ * Tokens read the shared --gbs-* variables when they are set on an ancestor
5
+ * (set them on :root to theme every component at once) and fall back to the
6
+ * same default palette otherwise. Rules live in the `components` layer, so utility classes passed
7
7
  * through `className` / `classNames` override them.
8
8
  */
9
9
  @layer theme, base, components, utilities;
10
10
 
11
11
  @layer components {
12
12
  .bc-root {
13
- --bc-fg: var(--dg-fg, light-dark(#18181b, #f4f4f5));
14
- --bc-muted: var(--dg-muted, light-dark(#71717a, #a1a1aa));
15
- --bc-hover: var(--dg-hover, light-dark(#f4f4f5, #1f1f23));
16
- --bc-focus: var(--dg-focus, light-dark(#2563eb, #60a5fa));
17
- --bc-font-size: var(--dg-font-size, 13px);
13
+ --bc-fg: var(--gbs-fg, var(--dg-fg, light-dark(#18181b, #f4f4f5)));
14
+ --bc-muted: var(--gbs-muted, var(--dg-muted, light-dark(#71717a, #a1a1aa)));
15
+ --bc-hover: var(--gbs-hover, var(--dg-hover, light-dark(#f4f4f5, #1f1f23)));
16
+ --bc-focus: var(--gbs-focus, var(--dg-focus, light-dark(#2563eb, #60a5fa)));
17
+ --bc-font-size: var(--gbs-font-size, var(--dg-font-size, 13px));
18
18
  /* Long labels are cut with an ellipsis; the full text is in the tooltip. */
19
19
  --bc-max-label: 24ch;
20
20
 
@@ -19,5 +19,5 @@ export interface ButtonRenderProps {
19
19
  [attribute: string]: unknown;
20
20
  }
21
21
 
22
- export const cx = (...names: (string | false | null | undefined)[]) =>
23
- names.filter(Boolean).join(" ");
22
+ // Shared with every other component, so class handling cannot drift.
23
+ export { cx } from "../../shared/core/cx";
@@ -1,27 +1,27 @@
1
1
  /*
2
2
  * Button styles.
3
3
  *
4
- * Tokens read the DataGrid's --dg-* variables when they are set on an ancestor
5
- * (set them on :root to share one theme) and fall back to the same palette
6
- * otherwise. Rules live in the `components` layer, so utility classes passed
4
+ * Tokens read the shared --gbs-* variables when they are set on an ancestor
5
+ * (set them on :root to theme every component at once) and fall back to the
6
+ * same default palette otherwise. Rules live in the `components` layer, so utility classes passed
7
7
  * through `className` / `classNames` override them.
8
8
  */
9
9
  @layer theme, base, components, utilities;
10
10
 
11
11
  @layer components {
12
12
  .bt-root {
13
- --bt-bg: var(--dg-bg, light-dark(#ffffff, #0b0b0e));
14
- --bt-fg: var(--dg-fg, light-dark(#18181b, #f4f4f5));
15
- --bt-border: var(--dg-border, light-dark(#e4e4e7, #27272a));
16
- --bt-hover: var(--dg-hover, light-dark(#f4f4f5, #1f1f23));
17
- --bt-subtle: light-dark(#f4f4f5, #1c1c20);
18
- --bt-accent: var(--dg-accent, light-dark(#2563eb, #60a5fa));
19
- --bt-accent-fg: var(--dg-accent-fg, light-dark(#ffffff, #0b1220));
20
- --bt-danger: var(--dg-danger, light-dark(#dc2626, #f87171));
21
- --bt-danger-fg: light-dark(#ffffff, #1c0606);
22
- --bt-focus: var(--dg-focus, light-dark(#2563eb, #60a5fa));
23
- --bt-radius: var(--dg-radius, 8px);
24
- --bt-font-size: var(--dg-font-size, 13px);
13
+ --bt-bg: var(--gbs-bg, var(--dg-bg, light-dark(#ffffff, #0b0b0e)));
14
+ --bt-fg: var(--gbs-fg, var(--dg-fg, light-dark(#18181b, #f4f4f5)));
15
+ --bt-border: var(--gbs-border, var(--dg-border, light-dark(#e4e4e7, #27272a)));
16
+ --bt-hover: var(--gbs-hover, var(--dg-hover, light-dark(#f4f4f5, #1f1f23)));
17
+ --bt-subtle: var(--gbs-subtle, light-dark(#f4f4f5, #1c1c20));
18
+ --bt-accent: var(--gbs-accent, var(--dg-accent, light-dark(#2563eb, #60a5fa)));
19
+ --bt-accent-fg: var(--gbs-accent-fg, var(--dg-accent-fg, light-dark(#ffffff, #0b1220)));
20
+ --bt-danger: var(--gbs-danger, var(--dg-danger, light-dark(#dc2626, #f87171)));
21
+ --bt-danger-fg: var(--gbs-danger-fg, light-dark(#ffffff, #1c0606));
22
+ --bt-focus: var(--gbs-focus, var(--dg-focus, light-dark(#2563eb, #60a5fa)));
23
+ --bt-radius: var(--gbs-radius, var(--dg-radius, 8px));
24
+ --bt-font-size: var(--gbs-font-size, var(--dg-font-size, 13px));
25
25
  --bt-height: 36px;
26
26
  --bt-px: 14px;
27
27
  --bt-gap: 8px;
@@ -0,0 +1,60 @@
1
+ # Card
2
+
3
+ A surface that groups related content, plus `Stat` for a single figure. For
4
+ React 19, with no runtime dependencies besides React.
5
+
6
+ ```ts
7
+ import { Card, CardHeader, CardBody, CardFooter, Stat, trendDirection } from "@/components/card";
8
+ import "@/components/card/styles.css";
9
+ ```
10
+
11
+ ```tsx
12
+ <Card>
13
+ <CardHeader title={<h3>Revenue</h3>} actions={<Menu trigger={<Button variant="ghost">…</Button>}>…</Menu>} />
14
+ <CardBody>
15
+ <Stat
16
+ label="This month"
17
+ value="£48,120"
18
+ trend={{ direction: trendDirection(12.4), label: "12.4%", description: "vs last month" }}
19
+ />
20
+ </CardBody>
21
+ </Card>
22
+ ```
23
+
24
+ ## Card
25
+
26
+ | Prop | Default | Description |
27
+ | --- | --- | --- |
28
+ | `variant` | `outlined` | `outlined`, `elevated` or `plain`. |
29
+ | `padding` | `md` | `none`, `sm`, `md` or `lg`. |
30
+ | `href` / `target` / `rel` | — | Makes the whole card a link. |
31
+ | `interactive` | `false` | Hover and focus styles for a card clickable another way. |
32
+ | `className`, `classNames`, `style` | — | Slots: `root`, `header`, `title`, `description`, `actions`, `body`, `footer`. |
33
+
34
+ `CardHeader` takes `title`, `description` and `actions`. Pass your own heading
35
+ element as the title — `<h3>Revenue</h3>` — so the page decides the level; a card
36
+ is not always at the same depth in the outline.
37
+
38
+ When `href` is set the card renders as an `<a>`, so keep other links and buttons
39
+ out of it: nesting interactive elements is not valid HTML and traps the
40
+ keyboard. Use `interactive` with your own handler instead.
41
+
42
+ ## Stat
43
+
44
+ | Prop | Description |
45
+ | --- | --- |
46
+ | `label` / `value` | Always both: a number on its own means nothing. |
47
+ | `trend` | `{ direction, label, invert?, description? }`. |
48
+ | `help` | A note under the figure. |
49
+ | `icon` | Drawn beside the label. |
50
+ | `loading` | Placeholder bars, announced as "Loading". |
51
+
52
+ `trend.invert` is for figures where down is the good outcome, such as churn or
53
+ error rates: it changes the colour, and the arrow still points the way the
54
+ number actually moved. The direction is also written out for screen readers, so
55
+ the meaning never rests on colour alone.
56
+
57
+ `trendDirection(change, threshold?)`, `isPositive(direction, invert?)` and
58
+ `percentChange(from, to)` are framework-free helpers in `core/trend.ts`.
59
+ `percentChange` returns `null` when the starting value is zero, because growth
60
+ from nothing is not a percentage.
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isPositive, percentChange, trendDirection } from "../core/trend";
3
+
4
+ describe("trendDirection", () => {
5
+ it("reads the sign of the change", () => {
6
+ expect(trendDirection(4.2)).toBe("up");
7
+ expect(trendDirection(-0.3)).toBe("down");
8
+ expect(trendDirection(0)).toBe("flat");
9
+ });
10
+
11
+ it("treats movement under the threshold as flat", () => {
12
+ expect(trendDirection(0.4, 0.5)).toBe("flat");
13
+ expect(trendDirection(-0.4, 0.5)).toBe("flat");
14
+ expect(trendDirection(0.6, 0.5)).toBe("up");
15
+ });
16
+
17
+ it("is flat for a value that is not a number", () => {
18
+ expect(trendDirection(Number.NaN)).toBe("flat");
19
+ expect(trendDirection(Number.POSITIVE_INFINITY)).toBe("flat");
20
+ });
21
+ });
22
+
23
+ describe("isPositive", () => {
24
+ it("treats up as good by default", () => {
25
+ expect(isPositive("up")).toBe(true);
26
+ expect(isPositive("down")).toBe(false);
27
+ });
28
+
29
+ it("flips for figures where down is the good outcome, such as churn", () => {
30
+ expect(isPositive("up", true)).toBe(false);
31
+ expect(isPositive("down", true)).toBe(true);
32
+ });
33
+
34
+ it("is null when nothing moved, so no colour is applied", () => {
35
+ expect(isPositive("flat")).toBeNull();
36
+ expect(isPositive("flat", true)).toBeNull();
37
+ });
38
+ });
39
+
40
+ describe("percentChange", () => {
41
+ it("is the change as a share of where it started", () => {
42
+ expect(percentChange(200, 250)).toBe(25);
43
+ expect(percentChange(200, 150)).toBe(-25);
44
+ });
45
+
46
+ it("measures against the size of a negative start", () => {
47
+ expect(percentChange(-200, -150)).toBe(25);
48
+ });
49
+
50
+ it("is null from zero, which has no percentage", () => {
51
+ expect(percentChange(0, 10)).toBeNull();
52
+ });
53
+
54
+ it("is null for values that are not numbers", () => {
55
+ expect(percentChange(Number.NaN, 10)).toBeNull();
56
+ });
57
+ });
@@ -0,0 +1,4 @@
1
+ // Framework-free: what counts as a rise or a fall.
2
+ export { isPositive, percentChange, trendDirection } from "./trend";
3
+ export type { TrendDirection } from "./trend";
4
+ export type * from "./types";
@@ -0,0 +1,32 @@
1
+ export type TrendDirection = "up" | "down" | "flat";
2
+
3
+ /** Whether a change counts as a rise, a fall, or no real movement. */
4
+ export function trendDirection(change: number, threshold = 0): TrendDirection {
5
+ if (!Number.isFinite(change)) return "flat";
6
+ if (change > threshold) return "up";
7
+ if (change < -threshold) return "down";
8
+ return "flat";
9
+ }
10
+
11
+ /**
12
+ * Whether a direction should be drawn as good news.
13
+ *
14
+ * Up is usually good, but not for churn, refunds or error rates, so `invert`
15
+ * flips it. Colour alone never carries the meaning: the arrow and the label do
16
+ * too.
17
+ */
18
+ export function isPositive(direction: TrendDirection, invert = false): boolean | null {
19
+ if (direction === "flat") return null;
20
+ return invert ? direction === "down" : direction === "up";
21
+ }
22
+
23
+ /**
24
+ * A change as a percentage of where it started.
25
+ *
26
+ * Returns null when there is nothing to compare against: growth from zero is
27
+ * not "infinite percent", it is simply not a percentage.
28
+ */
29
+ export function percentChange(from: number, to: number): number | null {
30
+ if (!Number.isFinite(from) || !Number.isFinite(to) || from === 0) return null;
31
+ return ((to - from) / Math.abs(from)) * 100;
32
+ }
@@ -0,0 +1,5 @@
1
+ export type CardVariant = "outlined" | "elevated" | "plain";
2
+ export type CardPadding = "none" | "sm" | "md" | "lg";
3
+
4
+ export type CardSlot = "root" | "header" | "title" | "description" | "actions" | "body" | "footer";
5
+ export type StatSlot = "root" | "label" | "value" | "trend" | "help" | "icon";
@@ -0,0 +1,5 @@
1
+ export { Card, CardBody, CardFooter, CardHeader } from "./react/Card";
2
+ export type { CardHeaderProps, CardProps } from "./react/Card";
3
+ export { Stat } from "./react/Stat";
4
+ export type { StatProps, StatTrend } from "./react/Stat";
5
+ export * from "./core";