gbs-add-block 1.2.9 → 1.2.11

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 (75) hide show
  1. package/README.md +12 -2
  2. package/index.cjs +85 -32
  3. package/package.json +1 -1
  4. package/source/beta-components/datepicker/README.md +141 -0
  5. package/source/beta-components/datepicker/__tests__/core.test.ts +220 -0
  6. package/source/beta-components/datepicker/core/calendar.ts +263 -0
  7. package/source/beta-components/datepicker/core/format.ts +170 -0
  8. package/source/beta-components/datepicker/core/index.ts +40 -0
  9. package/source/beta-components/datepicker/core/types.ts +68 -0
  10. package/source/beta-components/datepicker/index.ts +16 -0
  11. package/source/beta-components/datepicker/react/Calendar.tsx +160 -0
  12. package/source/beta-components/datepicker/react/CalendarPanel.tsx +170 -0
  13. package/source/beta-components/datepicker/react/DatePicker.tsx +249 -0
  14. package/source/beta-components/datepicker/react/DateRangePicker.tsx +291 -0
  15. package/source/beta-components/datepicker/react/MonthYearPanels.tsx +123 -0
  16. package/source/beta-components/datepicker/react/Popover.tsx +87 -0
  17. package/source/beta-components/datepicker/react/icons.tsx +32 -0
  18. package/source/beta-components/datepicker/react/locale.ts +23 -0
  19. package/source/beta-components/datepicker/react/props.ts +76 -0
  20. package/source/beta-components/datepicker/react/useDatePicker.ts +547 -0
  21. package/source/beta-components/datepicker/styles.css +446 -0
  22. /package/source/{components → beta-components}/combobox/README.md +0 -0
  23. /package/source/{components → beta-components}/combobox/__tests__/core.test.ts +0 -0
  24. /package/source/{components → beta-components}/combobox/core/filter.ts +0 -0
  25. /package/source/{components → beta-components}/combobox/core/index.ts +0 -0
  26. /package/source/{components → beta-components}/combobox/core/types.ts +0 -0
  27. /package/source/{components → beta-components}/combobox/core/virtual.ts +0 -0
  28. /package/source/{components → beta-components}/combobox/index.ts +0 -0
  29. /package/source/{components → beta-components}/combobox/react/Listbox.tsx +0 -0
  30. /package/source/{components → beta-components}/combobox/react/MultiSelect.tsx +0 -0
  31. /package/source/{components → beta-components}/combobox/react/Popover.tsx +0 -0
  32. /package/source/{components → beta-components}/combobox/react/Select.tsx +0 -0
  33. /package/source/{components → beta-components}/combobox/react/icons.tsx +0 -0
  34. /package/source/{components → beta-components}/combobox/react/locale.ts +0 -0
  35. /package/source/{components → beta-components}/combobox/react/props.ts +0 -0
  36. /package/source/{components → beta-components}/combobox/react/useCombobox.ts +0 -0
  37. /package/source/{components → beta-components}/combobox/styles.css +0 -0
  38. /package/source/{components/datagridbeta → beta-components/datagrid}/README.md +0 -0
  39. /package/source/{components/datagridbeta → beta-components/datagrid}/__tests__/core.test.ts +0 -0
  40. /package/source/{components/datagridbeta → beta-components/datagrid}/__tests__/export.test.ts +0 -0
  41. /package/source/{components/datagridbeta → beta-components/datagrid}/core/columnHelper.ts +0 -0
  42. /package/source/{components/datagridbeta → beta-components/datagrid}/core/columns.ts +0 -0
  43. /package/source/{components/datagridbeta → beta-components/datagrid}/core/filtering.ts +0 -0
  44. /package/source/{components/datagridbeta → beta-components/datagrid}/core/grid.ts +0 -0
  45. /package/source/{components/datagridbeta → beta-components/datagrid}/core/index.ts +0 -0
  46. /package/source/{components/datagridbeta → beta-components/datagrid}/core/rows.ts +0 -0
  47. /package/source/{components/datagridbeta → beta-components/datagrid}/core/sorting.ts +0 -0
  48. /package/source/{components/datagridbeta → beta-components/datagrid}/core/state.ts +0 -0
  49. /package/source/{components/datagridbeta → beta-components/datagrid}/core/store.ts +0 -0
  50. /package/source/{components/datagridbeta → beta-components/datagrid}/core/types.ts +0 -0
  51. /package/source/{components/datagridbeta → beta-components/datagrid}/core/values.ts +0 -0
  52. /package/source/{components/datagridbeta → beta-components/datagrid}/core/virtual.ts +0 -0
  53. /package/source/{components/datagridbeta → beta-components/datagrid}/export/csv.ts +0 -0
  54. /package/source/{components/datagridbeta → beta-components/datagrid}/export/download.ts +0 -0
  55. /package/source/{components/datagridbeta → beta-components/datagrid}/export/pdf.ts +0 -0
  56. /package/source/{components/datagridbeta → beta-components/datagrid}/export/table.ts +0 -0
  57. /package/source/{components/datagridbeta → beta-components/datagrid}/export/xlsx.ts +0 -0
  58. /package/source/{components/datagridbeta → beta-components/datagrid}/export/zip.ts +0 -0
  59. /package/source/{components/datagridbeta → beta-components/datagrid}/index.ts +0 -0
  60. /package/source/{components/datagridbeta → beta-components/datagrid}/react/Cell.tsx +0 -0
  61. /package/source/{components/datagridbeta → beta-components/datagrid}/react/ColumnMenu.tsx +0 -0
  62. /package/source/{components/datagridbeta → beta-components/datagrid}/react/DataGrid.tsx +0 -0
  63. /package/source/{components/datagridbeta → beta-components/datagrid}/react/FilterForm.tsx +0 -0
  64. /package/source/{components/datagridbeta → beta-components/datagrid}/react/HeaderRow.tsx +0 -0
  65. /package/source/{components/datagridbeta → beta-components/datagrid}/react/Pagination.tsx +0 -0
  66. /package/source/{components/datagridbeta → beta-components/datagrid}/react/Popover.tsx +0 -0
  67. /package/source/{components/datagridbeta → beta-components/datagrid}/react/Row.tsx +0 -0
  68. /package/source/{components/datagridbeta → beta-components/datagrid}/react/Toolbar.tsx +0 -0
  69. /package/source/{components/datagridbeta → beta-components/datagrid}/react/Viewport.tsx +0 -0
  70. /package/source/{components/datagridbeta → beta-components/datagrid}/react/context.ts +0 -0
  71. /package/source/{components/datagridbeta → beta-components/datagrid}/react/hooks.ts +0 -0
  72. /package/source/{components/datagridbeta → beta-components/datagrid}/react/icons.tsx +0 -0
  73. /package/source/{components/datagridbeta → beta-components/datagrid}/react/keyboard.ts +0 -0
  74. /package/source/{components/datagridbeta → beta-components/datagrid}/react/locale.ts +0 -0
  75. /package/source/{components/datagridbeta → beta-components/datagrid}/styles.css +0 -0
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # GBS Building Blocks 2.0 (v1.2.9)
1
+ # GBS Building Blocks 2.0 (v1.2.11)
2
2
 
3
3
  Latest and upgraded version of GBS building blocks with headless UI and removed dependencies.
4
4
 
@@ -6,7 +6,17 @@ Latest and upgraded version of GBS building blocks with headless UI and removed
6
6
 
7
7
  For detailed documentation on usage and props, Please visit: [Building Block Documentation v2.0](https://gramprokit.vercel.app)
8
8
 
9
- ## What's New 🎉 (Ver 1.2.9)
9
+ ## Beta Components
10
+
11
+ DataGrid and Combobox use the redesigned core API and design system. Install them with the beta flag:
12
+
13
+ ```bash
14
+ npx gbs-add-block -a DataGrid -beta
15
+ npx gbs-add-block -a Combobox -beta
16
+ npx gbs-add-block -a DatePicker -beta
17
+ ```
18
+
19
+ ## What's New 🎉 (Ver 1.2.11)
10
20
 
11
21
  - Update Candidate for next major change 2.0.0
12
22
 
package/index.cjs CHANGED
@@ -34,9 +34,8 @@ const CONFIG = {
34
34
  "Bargraph",
35
35
  "UsePaginatedData",
36
36
  "UseUploader",
37
- "DataGridBeta",
38
- "Combobox"
39
37
  ],
38
+ betaComponents: ["DataGrid", "Combobox", "DatePicker"],
40
39
  // Define component dependencies
41
40
  dependencies: {
42
41
  FormRenderer: ["Select", "MultiSelect", "Input", "DatePicker"],
@@ -45,8 +44,19 @@ const CONFIG = {
45
44
  };
46
45
 
47
46
  const SOURCE_PATH = path.join(__dirname, "source", "components");
47
+ const BETA_SOURCE_PATH = path.join(__dirname, "source", "beta-components");
48
48
  const DEFAULT_DEST_PATH = path.join(process.cwd(), "component-lib");
49
49
 
50
+ const normalizeComponent = (component, availableComponents) =>
51
+ availableComponents.find(
52
+ (available) => available.toLowerCase() === component.toLowerCase(),
53
+ );
54
+
55
+ const getAvailableComponents = (beta = false) =>
56
+ beta ? CONFIG.betaComponents : CONFIG.components;
57
+
58
+ const getSourcePath = (beta = false) => (beta ? BETA_SOURCE_PATH : SOURCE_PATH);
59
+
50
60
  const copyCommonFiles = async (destPath) => {
51
61
  const commonFiles = [
52
62
  { src: ["..", "utils.ts"], dest: "utils.ts" },
@@ -68,13 +78,18 @@ const checkComponentExists = (component, destPath) => {
68
78
  return fs.existsSync(componentPath);
69
79
  };
70
80
 
71
- const copyComponent = async (component, destPath) => {
81
+ const copyComponent = async (component, destPath, beta = false) => {
72
82
  try {
73
- const componentSrc = path.join(SOURCE_PATH, component.toLowerCase());
83
+ const componentSrc = path.join(
84
+ getSourcePath(beta),
85
+ component.toLowerCase(),
86
+ );
74
87
  const componentDest = path.join(destPath, component.toLowerCase());
75
88
 
76
89
  if (!fs.existsSync(componentSrc)) {
77
- throw new Error(`Component ${component} not found in source directory.`);
90
+ throw new Error(
91
+ `Component ${component} not found in ${beta ? "beta " : ""}source directory.`,
92
+ );
78
93
  }
79
94
 
80
95
  await fs.copy(componentSrc, componentDest, { overwrite: true });
@@ -91,15 +106,21 @@ const copyComponent = async (component, destPath) => {
91
106
  }
92
107
  };
93
108
 
94
- const installComponentWithDependencies = async (component, destPath) => {
109
+ const installComponentWithDependencies = async (
110
+ component,
111
+ destPath,
112
+ beta = false,
113
+ ) => {
95
114
  // Get dependencies for the component
96
115
  const dependencies = CONFIG.dependencies[component] || [];
97
116
  const componentsToInstall = new Set([component, ...dependencies]);
98
117
 
99
118
  // Check which components need to be installed
100
- const pendingInstalls = Array.from(componentsToInstall).filter(
101
- (comp) => !checkComponentExists(comp, destPath),
102
- );
119
+ const pendingInstalls = beta
120
+ ? Array.from(componentsToInstall)
121
+ : Array.from(componentsToInstall).filter(
122
+ (comp) => !checkComponentExists(comp, destPath),
123
+ );
103
124
 
104
125
  if (pendingInstalls.length === 0) {
105
126
  console.log(
@@ -110,7 +131,7 @@ const installComponentWithDependencies = async (component, destPath) => {
110
131
 
111
132
  // Install all pending components
112
133
  for (const comp of pendingInstalls) {
113
- await copyComponent(comp, destPath);
134
+ await copyComponent(comp, destPath, beta);
114
135
  }
115
136
 
116
137
  if (dependencies.length > 0) {
@@ -123,7 +144,11 @@ const installComponentWithDependencies = async (component, destPath) => {
123
144
  console.log(`\nFor documentation visit: ${CONFIG.docs}`);
124
145
  };
125
146
 
126
- const installMultipleComponents = async (components, destPath) => {
147
+ const installMultipleComponents = async (
148
+ components,
149
+ destPath,
150
+ beta = false,
151
+ ) => {
127
152
  const allComponentsToInstall = new Set();
128
153
 
129
154
  // Collect all components and their dependencies
@@ -134,9 +159,11 @@ const installMultipleComponents = async (components, destPath) => {
134
159
  });
135
160
 
136
161
  // Filter out already installed components
137
- const pendingInstalls = Array.from(allComponentsToInstall).filter(
138
- (comp) => !checkComponentExists(comp, destPath),
139
- );
162
+ const pendingInstalls = beta
163
+ ? Array.from(allComponentsToInstall)
164
+ : Array.from(allComponentsToInstall).filter(
165
+ (comp) => !checkComponentExists(comp, destPath),
166
+ );
140
167
 
141
168
  if (pendingInstalls.length === 0) {
142
169
  console.log(
@@ -149,7 +176,7 @@ const installMultipleComponents = async (components, destPath) => {
149
176
 
150
177
  // Install all pending components
151
178
  for (const comp of pendingInstalls) {
152
- await copyComponent(comp, destPath);
179
+ await copyComponent(comp, destPath, beta);
153
180
  }
154
181
 
155
182
  // Show dependency information
@@ -169,7 +196,7 @@ const installMultipleComponents = async (components, destPath) => {
169
196
  console.log(`\nFor documentation visit: ${CONFIG.docs}`);
170
197
  };
171
198
 
172
- const interactiveComponentSelector = async () => {
199
+ const interactiveComponentSelector = async (beta = false) => {
173
200
  return new Promise((resolve) => {
174
201
  const rl = readline.createInterface({
175
202
  input: process.stdin,
@@ -186,7 +213,9 @@ const interactiveComponentSelector = async () => {
186
213
  "Use ↑/↓ arrow keys to navigate, SPACE to select/deselect, ENTER to install\n",
187
214
  );
188
215
 
189
- CONFIG.components.forEach((component, index) => {
216
+ const availableComponents = getAvailableComponents(beta);
217
+
218
+ availableComponents.forEach((component, index) => {
190
219
  const isSelected = selectedComponents.has(component);
191
220
  const isCurrentIndex = index === currentIndex;
192
221
  const deps = CONFIG.dependencies[component]
@@ -216,13 +245,13 @@ const interactiveComponentSelector = async () => {
216
245
  break;
217
246
  case "\u001b[B": // Down arrow
218
247
  currentIndex = Math.min(
219
- CONFIG.components.length - 1,
248
+ getAvailableComponents(beta).length - 1,
220
249
  currentIndex + 1,
221
250
  );
222
251
  renderMenu();
223
252
  break;
224
253
  case " ": // Space bar
225
- const component = CONFIG.components[currentIndex];
254
+ const component = getAvailableComponents(beta)[currentIndex];
226
255
  if (selectedComponents.has(component)) {
227
256
  selectedComponents.delete(component);
228
257
  } else {
@@ -265,21 +294,29 @@ const parseMultipleComponents = (componentString) => {
265
294
  .filter((comp) => comp.length > 0);
266
295
  };
267
296
 
268
- const validateComponents = (components) => {
297
+ const validateComponents = (components, beta = false) => {
298
+ const availableComponents = getAvailableComponents(beta);
269
299
  const invalidComponents = components.filter(
270
- (comp) => !CONFIG.components.includes(comp),
300
+ (comp) => !availableComponents.includes(comp),
271
301
  );
272
302
  if (invalidComponents.length > 0) {
273
303
  console.error(`Invalid components: ${invalidComponents.join(", ")}`);
274
- console.log("\nAvailable components:");
275
- CONFIG.components.forEach((comp) => console.log(`- ${comp}`));
304
+ console.log(`\nAvailable ${beta ? "beta " : ""}components:`);
305
+ availableComponents.forEach((comp) => console.log(`- ${comp}`));
306
+ if (!beta) {
307
+ console.log("\nRedesigned beta components (install with -beta):");
308
+ CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
309
+ }
276
310
  return false;
277
311
  }
278
312
  return true;
279
313
  };
280
314
 
281
315
  const main = async () => {
282
- const argv = yargs(hideBin(process.argv))
316
+ const args = hideBin(process.argv).map((arg) =>
317
+ arg === "-beta" ? "--beta" : arg,
318
+ );
319
+ const argv = yargs(args)
283
320
  .option("add", {
284
321
  alias: "a",
285
322
  describe:
@@ -296,27 +333,39 @@ const main = async () => {
296
333
  describe: "List available components",
297
334
  type: "boolean",
298
335
  })
336
+ .option("beta", {
337
+ describe: "Install redesigned beta components",
338
+ type: "boolean",
339
+ default: false,
340
+ })
299
341
  .example("$0 -a Button", "Install a single component")
300
342
  .example("$0 -a Button,Card,Modal", "Install multiple components")
343
+ .example("$0 -a DataGrid -beta", "Install the redesigned beta DataGrid")
344
+ .example("$0 -a Combobox -beta", "Install the redesigned beta Combobox")
301
345
  .example("$0 -i", "Interactive selection mode")
302
346
  .help().argv;
303
347
 
304
348
  // List components if requested
305
349
  if (argv.list) {
306
- console.log("\nAvailable components:");
307
- CONFIG.components.forEach((comp) => {
350
+ const availableComponents = getAvailableComponents(argv.beta);
351
+ console.log(`\nAvailable ${argv.beta ? "beta " : ""}components:`);
352
+ availableComponents.forEach((comp) => {
308
353
  const deps = CONFIG.dependencies[comp]
309
354
  ? ` (requires: ${CONFIG.dependencies[comp].join(", ")})`
310
355
  : "";
311
356
  console.log(`- ${comp}${deps}`);
312
357
  });
358
+ if (!argv.beta) {
359
+ console.log("\nRedesigned beta components (install with -beta):");
360
+ CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
361
+ }
313
362
  return;
314
363
  }
315
364
 
316
365
  // Interactive mode
317
366
  if (argv.interactive) {
318
367
  console.log("Starting interactive component selector...\n");
319
- const selectedComponents = await interactiveComponentSelector();
368
+ const selectedComponents = await interactiveComponentSelector(argv.beta);
320
369
 
321
370
  if (selectedComponents.length === 0) {
322
371
  console.log("No components selected. Exiting...");
@@ -333,7 +382,7 @@ const main = async () => {
333
382
  }
334
383
 
335
384
  // Install selected components
336
- await installMultipleComponents(selectedComponents, destPath);
385
+ await installMultipleComponents(selectedComponents, destPath, argv.beta);
337
386
  return;
338
387
  }
339
388
 
@@ -346,10 +395,14 @@ const main = async () => {
346
395
 
347
396
  // Parse components (single or multiple)
348
397
  const componentInput = argv.add;
349
- const components = parseMultipleComponents(componentInput);
398
+ const components = parseMultipleComponents(componentInput).map(
399
+ (component) =>
400
+ normalizeComponent(component, getAvailableComponents(argv.beta)) ||
401
+ component,
402
+ );
350
403
 
351
404
  // Validate all components
352
- if (!validateComponents(components)) {
405
+ if (!validateComponents(components, argv.beta)) {
353
406
  process.exit(1);
354
407
  }
355
408
 
@@ -365,10 +418,10 @@ const main = async () => {
365
418
  // Install components
366
419
  if (components.length === 1) {
367
420
  // Single component installation (existing behavior)
368
- await installComponentWithDependencies(components[0], destPath);
421
+ await installComponentWithDependencies(components[0], destPath, argv.beta);
369
422
  } else {
370
423
  // Multiple components installation
371
- await installMultipleComponents(components, destPath);
424
+ await installMultipleComponents(components, destPath, argv.beta);
372
425
  }
373
426
  };
374
427
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbs-add-block",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "React Component Library",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,141 @@
1
+ # DatePicker and DateRangePicker
2
+
3
+ Two date fields built on one engine and styled to match the DataGrid and the
4
+ Combobox. No runtime dependencies besides React 19.
5
+
6
+ - **`<DatePicker>`** — one date.
7
+ - **`<DateRangePicker>`** — a start and an end, with a live preview between them.
8
+
9
+ Both support typed entry in the user's own date order, min/max limits, blocked
10
+ days, month and year panels, week numbers, forms, full keyboard control, dark
11
+ mode and right-to-left layouts.
12
+
13
+ ## Setup
14
+
15
+ ```ts
16
+ import { DatePicker, DateRangePicker } from "@/components/date-picker";
17
+ import "@/components/date-picker/styles.css";
18
+ ```
19
+
20
+ The stylesheet reuses the DataGrid's `--dg-*` variables when that stylesheet is
21
+ loaded, so every component shares a theme, and falls back to the same palette
22
+ when used on its own.
23
+
24
+ ## Basic usage
25
+
26
+ ```tsx
27
+ const [date, setDate] = useState<Date | null>(null);
28
+
29
+ <DatePicker label="Start date" value={date} onChange={setDate} />;
30
+
31
+ const [range, setRange] = useState<DateRange>({ start: null, end: null });
32
+
33
+ <DateRangePicker label="Period" value={range} onChange={setRange} />;
34
+ ```
35
+
36
+ Both are controlled with `value` + `onChange`, or uncontrolled with
37
+ `defaultValue`. `value`, `defaultValue`, `min` and `max` accept a `Date`, an ISO
38
+ `yyyy-mm-dd` string or a timestamp; `onChange` always gives you `Date` objects at
39
+ local midnight. The range picker's `onChange` also receives a `complete` flag,
40
+ which is `false` after the first of the two clicks.
41
+
42
+ ## Props
43
+
44
+ Shared by both components:
45
+
46
+ | Prop | Type | Default | Description |
47
+ | --- | --- | --- | --- |
48
+ | `min` / `max` | `Date \| string \| number` | — | Earliest and latest selectable day. |
49
+ | `isDateDisabled` | `(date: Date) => boolean` | — | Called per rendered day; return true to block it. |
50
+ | `locale` | `string` | runtime locale | Decides month names, field order and the first day of the week. |
51
+ | `weekStartsOn` | `0`–`6` | locale's first day | 0 is Sunday. |
52
+ | `format` | `Intl.DateTimeFormatOptions \| (date, locale) => string` | `{ year, month: "short", day }` | How the chosen date is written in the field. |
53
+ | `numberOfMonths` | `number` | `1` (range: `2`) | Months shown side by side. |
54
+ | `showWeekNumbers` | `boolean` | `false` | Adds an ISO week column. |
55
+ | `showToday` | `boolean` | `true` (range: `false`) | The "Today" shortcut in the footer. |
56
+ | `allowInput` | `boolean` | `true` | Let people type a date. When false the field is read-only and opens on click. |
57
+ | `fixedWeeks` | `boolean` | `true` | Always six week rows, so the popover never changes height. |
58
+ | `label`, `description`, `error` | `ReactNode` | — | Field label, hint and error message. `error` also marks the control invalid. |
59
+ | `placeholder` | `string` | the locale's pattern | e.g. `dd/mm/yyyy`. |
60
+ | `required`, `disabled`, `readOnly` | `boolean` | `false` | Field states. |
61
+ | `clearable` | `boolean` | `true` | Show the clear button. |
62
+ | `size` | `"sm"` \| `"md"` \| `"lg"` | `"md"` | Control height and font size. |
63
+ | `name` | `string` | — | Posts `yyyy-mm-dd` in a hidden input. The range picker posts `name-start` and `name-end`. |
64
+ | `className`, `classNames`, `style` | — | — | Styling hooks. Slots: `root`, `label`, `control`, `input`, `popover`, `calendar`, `day`, `footer`, `presets`. |
65
+ | `localeText` | `Partial<DatePickerLocaleText>` | English | Overrides UI text. |
66
+ | `onOpenChange` | `(open: boolean) => void` | — | Fires when the calendar opens or closes. |
67
+ | `ref` | `Ref<DatePickerHandle<T>>` | — | `open()`, `close()`, `toggle()`, `focus()`, `clear()`, `getValue()`. |
68
+
69
+ `DatePicker` adds `value` / `defaultValue` (`Date \| null`) and `closeOnSelect`
70
+ (default `true`). `DateRangePicker` adds `value` / `defaultValue`
71
+ (`{ start, end }`), `presets` and `closeOnSelect` (closes once both ends are set).
72
+
73
+ ## Typed entry
74
+
75
+ People can type instead of picking. Input is read in the locale's own field
76
+ order, so `03/04/2026` is 3 April in `en-GB` and 4 March in `en-US`. ISO
77
+ (`2026-03-12`) is accepted in every locale, month names work (`12 Mar 2026`), and
78
+ a missing year or month falls back to the month on screen. The calendar follows
79
+ along as you type. Text that isn't a date, or a date outside the limits, shows a
80
+ message when the field loses focus and leaves the value untouched.
81
+
82
+ ## Range presets
83
+
84
+ ```tsx
85
+ <DateRangePicker
86
+ value={range}
87
+ onChange={setRange}
88
+ presets={[
89
+ { label: "Last 7 days", range: { start: addDays(new Date(), -6), end: new Date() } },
90
+ { label: "This month", range: { start: startOfMonth(new Date()), end: new Date() } },
91
+ ]}
92
+ />
93
+ ```
94
+
95
+ ## Keyboard
96
+
97
+ | Keys | Action |
98
+ | --- | --- |
99
+ | **Enter**, **↓** | Open the calendar (from the field). |
100
+ | **←** / **→** | Previous / next day (swapped in right-to-left layouts). |
101
+ | **↑** / **↓** | Same weekday, previous / next week. |
102
+ | **Home** / **End** | First / last day of the week. |
103
+ | **Page Up** / **Page Down** | Previous / next month. |
104
+ | **Shift + Page Up/Down** | Previous / next year. |
105
+ | **Enter**, **Space** | Choose the focused day. |
106
+ | **Escape** | Close and return focus to the calendar button. |
107
+
108
+ Each month is a `role="grid"` where only the focused day is tabbable, which is
109
+ the WAI-ARIA pattern for a date picker. The calendar renders in the top layer
110
+ through the native Popover API, so it is never clipped by a scrolling parent
111
+ (including inside a DataGrid cell).
112
+
113
+ ## Theming
114
+
115
+ Override `--dp-*` variables (they default to the grid's `--dg-*`):
116
+
117
+ ```css
118
+ .dp-root { --dp-accent: #7c3aed; --dp-radius: 12px; --dp-day-size: 38px; }
119
+ ```
120
+
121
+ State attributes for styling: `data-state="open"`, `data-size`, `data-invalid`,
122
+ `data-disabled` on the root and control; `data-today`, `data-selected`,
123
+ `data-outside`, `data-weekend` on days; `data-in-range`, `data-range-start`,
124
+ `data-range-end` on day cells.
125
+
126
+ ## Headless use
127
+
128
+ `useDatePicker()` holds the whole engine (selection, visible months, typed
129
+ input, keyboard movement, popover state) and is exported if you want a different
130
+ UI on top. The framework-free helpers — `buildMonth`, `buildMonths`, `parseDate`,
131
+ `formatDate`, `moveByKey`, `normalizeRange`, `isInRange`, `toISODate` — are
132
+ exported from `@/components/date-picker/core` and also run on a server.
133
+
134
+ ## Known limits
135
+
136
+ - Dates only: no time-of-day or time zone selection. Values are local midnight.
137
+ - One range per field; multiple disjoint ranges aren't supported.
138
+ - Only the Gregorian calendar is handled, though month and weekday names,
139
+ field order and the first day of the week all follow the locale.
140
+ - When rendering on a server, pass `locale` explicitly so the server and the
141
+ browser format the field identically.
@@ -0,0 +1,220 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ addDays,
4
+ addMonths,
5
+ buildMonth,
6
+ buildMonths,
7
+ clampDate,
8
+ compareDay,
9
+ getISOWeek,
10
+ isInRange,
11
+ isSameDay,
12
+ moveByKey,
13
+ moveInGrid,
14
+ normalizeRange,
15
+ parseISODate,
16
+ toDate,
17
+ toISODate,
18
+ yearPage,
19
+ } from "../core/calendar";
20
+ import {
21
+ fieldOrder,
22
+ inputPlaceholder,
23
+ monthNames,
24
+ parseDate,
25
+ weekdayNames,
26
+ } from "../core/format";
27
+
28
+ const iso = (date: Date | null) => (date ? toISODate(date) : null);
29
+
30
+ describe("date normalization", () => {
31
+ it("reads ISO strings as local days, never shifted by the time zone", () => {
32
+ expect(iso(toDate("2026-03-12"))).toBe("2026-03-12");
33
+ expect(iso(toDate(new Date(2026, 2, 12, 23, 59)))).toBe("2026-03-12");
34
+ expect(toDate(null)).toBeNull();
35
+ expect(toDate("not a date")).toBeNull();
36
+ expect(parseISODate("2026-02-31")).toBeNull();
37
+ });
38
+
39
+ it("compares and matches days regardless of the time", () => {
40
+ expect(compareDay(new Date(2026, 0, 1, 23), new Date(2026, 0, 2, 0))).toBeLessThan(0);
41
+ expect(isSameDay(new Date(2026, 0, 1, 23), new Date(2026, 0, 1))).toBe(true);
42
+ expect(isSameDay(null, new Date(2026, 0, 1))).toBe(false);
43
+ });
44
+
45
+ it("keeps the day of the month when a month is too short", () => {
46
+ expect(iso(addMonths(new Date(2026, 0, 31), 1))).toBe("2026-02-28");
47
+ expect(iso(addMonths(new Date(2024, 0, 31), 1))).toBe("2024-02-29");
48
+ expect(iso(addDays(new Date(2026, 11, 31), 1))).toBe("2027-01-01");
49
+ });
50
+
51
+ it("clamps to the limits", () => {
52
+ const min = new Date(2026, 0, 10);
53
+ const max = new Date(2026, 0, 20);
54
+ expect(iso(clampDate(new Date(2026, 0, 1), min, max))).toBe("2026-01-10");
55
+ expect(iso(clampDate(new Date(2026, 0, 31), min, max))).toBe("2026-01-20");
56
+ expect(iso(clampDate(new Date(2026, 0, 15), min, max))).toBe("2026-01-15");
57
+ });
58
+ });
59
+
60
+ describe("month building", () => {
61
+ const today = new Date(2026, 1, 10);
62
+
63
+ it("returns six aligned weeks with the leading days of the previous month", () => {
64
+ const february = buildMonth(2026, 1, { weekStartsOn: 1, today });
65
+ expect(february.weeks).toHaveLength(6);
66
+ expect(february.weeks[0].days).toHaveLength(7);
67
+ // February 2026 starts on a Sunday, so a Monday-first grid opens on 26 January.
68
+ expect(february.weeks[0].days[0].key).toBe("2026-01-26");
69
+ expect(february.weeks[0].days[0].outside).toBe(true);
70
+ expect(february.weeks[0].days[6].key).toBe("2026-02-01");
71
+ expect(february.weeks[0].days[6].outside).toBe(false);
72
+ });
73
+
74
+ it("starts the week where the caller asks", () => {
75
+ const sundayFirst = buildMonth(2026, 1, { weekStartsOn: 0, today });
76
+ expect(sundayFirst.weeks[0].days[0].key).toBe("2026-02-01");
77
+ });
78
+
79
+ it("marks today, weekends and days outside the limits", () => {
80
+ const february = buildMonth(2026, 1, {
81
+ weekStartsOn: 1,
82
+ today,
83
+ min: new Date(2026, 1, 5),
84
+ isDateDisabled: (date) => date.getDate() === 20,
85
+ });
86
+ const days = february.weeks.flatMap((week) => week.days);
87
+ const byKey = (key: string) => days.find((day) => day.key === key);
88
+
89
+ expect(byKey("2026-02-10")?.today).toBe(true);
90
+ expect(byKey("2026-02-09")?.today).toBe(false);
91
+ expect(byKey("2026-02-07")?.weekend).toBe(true);
92
+ expect(byKey("2026-02-04")?.disabled).toBe(true);
93
+ expect(byKey("2026-02-05")?.disabled).toBe(false);
94
+ expect(byKey("2026-02-20")?.disabled).toBe(true);
95
+ });
96
+
97
+ it("builds consecutive months for a range view", () => {
98
+ const months = buildMonths(new Date(2026, 10, 20), 2, { today });
99
+ expect(months.map((month) => `${month.year}-${month.month}`)).toEqual(["2026-10", "2026-11"]);
100
+ });
101
+
102
+ it("numbers ISO weeks, including the ones spanning a year end", () => {
103
+ expect(getISOWeek(new Date(2024, 0, 1))).toBe(1);
104
+ expect(getISOWeek(new Date(2026, 0, 1))).toBe(1);
105
+ expect(getISOWeek(new Date(2021, 0, 1))).toBe(53);
106
+ });
107
+ });
108
+
109
+ describe("ranges", () => {
110
+ it("puts a backwards range the right way round", () => {
111
+ const start = new Date(2026, 0, 20);
112
+ const end = new Date(2026, 0, 10);
113
+ expect(iso(normalizeRange({ start, end }).start)).toBe("2026-01-10");
114
+ expect(iso(normalizeRange({ start, end }).end)).toBe("2026-01-20");
115
+ expect(normalizeRange({ start, end: null }).end).toBeNull();
116
+ });
117
+
118
+ it("includes both ends", () => {
119
+ const start = new Date(2026, 0, 10);
120
+ const end = new Date(2026, 0, 20);
121
+ expect(isInRange(new Date(2026, 0, 10), start, end)).toBe(true);
122
+ expect(isInRange(new Date(2026, 0, 20), start, end)).toBe(true);
123
+ expect(isInRange(new Date(2026, 0, 21), start, end)).toBe(false);
124
+ expect(isInRange(new Date(2026, 0, 15), start, null)).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe("keyboard movement", () => {
129
+ const wednesday = new Date(2026, 1, 11);
130
+
131
+ it("moves by day, week and month", () => {
132
+ expect(iso(moveByKey(wednesday, "ArrowLeft"))).toBe("2026-02-10");
133
+ expect(iso(moveByKey(wednesday, "ArrowRight"))).toBe("2026-02-12");
134
+ expect(iso(moveByKey(wednesday, "ArrowUp"))).toBe("2026-02-04");
135
+ expect(iso(moveByKey(wednesday, "ArrowDown"))).toBe("2026-02-18");
136
+ expect(iso(moveByKey(wednesday, "PageUp"))).toBe("2026-01-11");
137
+ expect(iso(moveByKey(wednesday, "PageDown"))).toBe("2026-03-11");
138
+ expect(iso(moveByKey(wednesday, "PageUp", { shiftKey: true }))).toBe("2025-02-11");
139
+ expect(iso(moveByKey(wednesday, "PageDown", { shiftKey: true }))).toBe("2027-02-11");
140
+ expect(moveByKey(wednesday, "a")).toBeNull();
141
+ });
142
+
143
+ it("takes Home and End to the ends of the displayed week", () => {
144
+ expect(iso(moveByKey(wednesday, "Home", { weekStartsOn: 1 }))).toBe("2026-02-09");
145
+ expect(iso(moveByKey(wednesday, "End", { weekStartsOn: 1 }))).toBe("2026-02-15");
146
+ expect(iso(moveByKey(wednesday, "Home", { weekStartsOn: 0 }))).toBe("2026-02-08");
147
+ expect(iso(moveByKey(wednesday, "End", { weekStartsOn: 0 }))).toBe("2026-02-14");
148
+ });
149
+
150
+ it("swaps the arrows in right-to-left layouts", () => {
151
+ expect(iso(moveByKey(wednesday, "ArrowRight", { rtl: true }))).toBe("2026-02-10");
152
+ expect(iso(moveByKey(wednesday, "ArrowLeft", { rtl: true }))).toBe("2026-02-12");
153
+ });
154
+
155
+ it("moves inside the month and year panels without leaving the grid", () => {
156
+ expect(moveInGrid(4, "ArrowDown", 3, 12)).toBe(7);
157
+ expect(moveInGrid(10, "ArrowDown", 3, 12)).toBe(11);
158
+ expect(moveInGrid(1, "ArrowUp", 3, 12)).toBe(0);
159
+ expect(moveInGrid(5, "Home", 3, 12)).toBe(0);
160
+ expect(moveInGrid(5, "End", 3, 12)).toBe(11);
161
+ expect(moveInGrid(5, "Enter", 3, 12)).toBeNull();
162
+ });
163
+
164
+ it("pages years in steady blocks", () => {
165
+ expect(yearPage(2026, 12)[0]).toBe(2016);
166
+ expect(yearPage(2026, 12)).toHaveLength(12);
167
+ expect(yearPage(2016, 12)[0]).toBe(2016);
168
+ });
169
+ });
170
+
171
+ describe("locale formatting", () => {
172
+ it("reports the field order and a matching placeholder", () => {
173
+ expect(fieldOrder("en-GB")).toEqual(["day", "month", "year"]);
174
+ expect(fieldOrder("en-US")).toEqual(["month", "day", "year"]);
175
+ expect(inputPlaceholder("en-GB")).toBe("dd/mm/yyyy");
176
+ expect(inputPlaceholder("en-US")).toBe("mm/dd/yyyy");
177
+ });
178
+
179
+ it("orders weekday names from the first day of the week", () => {
180
+ expect(weekdayNames("en-GB", 1)[0].short).toBe("Mon");
181
+ expect(weekdayNames("en-US", 0)[0].short).toBe("Sun");
182
+ expect(weekdayNames("en-GB", 1)[0].long).toBe("Monday");
183
+ expect(monthNames("en-GB")[0]).toBe("January");
184
+ });
185
+ });
186
+
187
+ describe("typed input", () => {
188
+ const reference = new Date(2026, 4, 20);
189
+
190
+ it("accepts ISO whatever the locale", () => {
191
+ expect(iso(parseDate("2026-03-12", "en-US", reference))).toBe("2026-03-12");
192
+ });
193
+
194
+ it("reads numbers in the locale's own order", () => {
195
+ expect(iso(parseDate("3/4/2026", "en-GB", reference))).toBe("2026-04-03");
196
+ expect(iso(parseDate("3/4/2026", "en-US", reference))).toBe("2026-03-04");
197
+ expect(iso(parseDate("15.8.2026", "de-DE", reference))).toBe("2026-08-15");
198
+ });
199
+
200
+ it("fills in what was left out", () => {
201
+ expect(iso(parseDate("15/8", "en-GB", reference))).toBe("2026-08-15");
202
+ expect(iso(parseDate("5", "en-GB", reference))).toBe("2026-05-05");
203
+ expect(iso(parseDate("15/8/26", "en-GB", reference))).toBe("2026-08-15");
204
+ expect(iso(parseDate("15/8/95", "en-GB", reference))).toBe("1995-08-15");
205
+ });
206
+
207
+ it("understands written month names", () => {
208
+ expect(iso(parseDate("12 Mar 2026", "en-GB", reference))).toBe("2026-03-12");
209
+ expect(iso(parseDate("March 12, 2026", "en-US", reference))).toBe("2026-03-12");
210
+ expect(iso(parseDate("7 September", "en-GB", reference))).toBe("2026-09-07");
211
+ });
212
+
213
+ it("rejects anything that isn't a date", () => {
214
+ expect(parseDate("", "en-GB", reference)).toBeNull();
215
+ expect(parseDate("hello", "en-GB", reference)).toBeNull();
216
+ expect(parseDate("31/2/2026", "en-GB", reference)).toBeNull();
217
+ expect(parseDate("2026-02-31", "en-GB", reference)).toBeNull();
218
+ expect(parseDate("45/45/2026", "en-GB", reference)).toBeNull();
219
+ });
220
+ });