feat(ui): add DateRangePicker component
Adds a new DateRangePicker component to @backstage/ui built on React Aria, featuring a custom field group with two DateInput fields and a calendar trigger, a RangeCalendar popover with proper range selection visuals (solid circles for start/end, transparent solid fill for in-range cells with row-boundary rounding), and full BUI token usage including bg consumer auto-increment. Includes Storybook stories and a docs-ui page. Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Added new `DateRangePicker` component — combines two date fields and a calendar popover for selecting a date range, built on React Aria with full keyboard and screen reader accessibility. Uses BUI design tokens throughout, including auto-incremented backgrounds via the bg consumer pattern.
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { DateRangePicker } from '../../../../../packages/ui/src/components/DateRangePicker/DateRangePicker';
|
||||
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
|
||||
import { parseDate } from '@internationalized/date';
|
||||
|
||||
export const WithLabel = () => {
|
||||
return (
|
||||
<DateRangePicker
|
||||
label="Date range"
|
||||
description="Select a start and end date for your event."
|
||||
style={{ maxWidth: '360px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sizes = () => {
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
style={{ width: '100%', maxWidth: '360px' }}
|
||||
>
|
||||
<DateRangePicker label="Small" size="small" />
|
||||
<DateRangePicker label="Medium" size="medium" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithDefaultValue = () => {
|
||||
return (
|
||||
<DateRangePicker
|
||||
label="Booking period"
|
||||
defaultValue={{
|
||||
start: parseDate('2025-02-03'),
|
||||
end: parseDate('2025-02-14'),
|
||||
}}
|
||||
style={{ maxWidth: '360px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Disabled = () => {
|
||||
return (
|
||||
<DateRangePicker
|
||||
label="Date range"
|
||||
isDisabled
|
||||
style={{ maxWidth: '360px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { dateRangePickerPropDefs } from './props-definition';
|
||||
import {
|
||||
dateRangePickerUsageSnippet,
|
||||
withLabelSnippet,
|
||||
sizesSnippet,
|
||||
withDefaultValueSnippet,
|
||||
disabledSnippet,
|
||||
} from './snippets';
|
||||
import { WithLabel, Sizes, WithDefaultValue, Disabled } from './components';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { DateRangePickerDefinition } from '../../../utils/definitions';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { ReactAriaLink } from '@/components/ReactAriaLink';
|
||||
|
||||
export const reactAriaUrls = {
|
||||
dateRangePicker: 'https://react-spectrum.adobe.com/react-aria/DateRangePicker.html',
|
||||
};
|
||||
|
||||
<PageTitle
|
||||
title="DateRangePicker"
|
||||
description="A date range picker that combines two date fields and a calendar popover for selecting a start and end date."
|
||||
/>
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<WithLabel />}
|
||||
code={withLabelSnippet}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={dateRangePickerUsageSnippet} />
|
||||
|
||||
## API reference
|
||||
|
||||
<PropsTable data={dateRangePickerPropDefs} />
|
||||
|
||||
<ReactAriaLink component="DateRangePicker" href={reactAriaUrls.dateRangePicker} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<Sizes />}
|
||||
code={sizesSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
### With default value
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<WithDefaultValue />}
|
||||
code={withDefaultValueSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<Disabled />}
|
||||
code={disabledSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
<Theming definition={DateRangePickerDefinition} />
|
||||
|
||||
<ChangelogComponent component="date-range-picker" />
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
import { Chip } from '@/components/Chip';
|
||||
|
||||
export const dateRangePickerPropDefs: Record<string, PropDef> = {
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
default: 'small',
|
||||
responsive: true,
|
||||
description: (
|
||||
<>
|
||||
Visual size of the picker. Use <Chip>small</Chip> for dense layouts,{' '}
|
||||
<Chip>medium</Chip> for prominent fields.
|
||||
</>
|
||||
),
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'Visible label displayed above the picker.',
|
||||
},
|
||||
secondaryLabel: {
|
||||
type: 'string',
|
||||
description: (
|
||||
<>
|
||||
Secondary text shown next to the label. If not provided and isRequired
|
||||
is true, displays <Chip>Required</Chip>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Help text displayed below the label.',
|
||||
},
|
||||
value: {
|
||||
type: 'enum',
|
||||
values: ['RangeValue<DateValue>'],
|
||||
description: 'Controlled value of the date range.',
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'enum',
|
||||
values: ['RangeValue<DateValue>'],
|
||||
description: 'Default value for uncontrolled usage.',
|
||||
},
|
||||
onChange: {
|
||||
type: 'enum',
|
||||
values: ['(value: RangeValue<DateValue> | null) => void'],
|
||||
description: 'Handler called when the selected range changes.',
|
||||
},
|
||||
granularity: {
|
||||
type: 'enum',
|
||||
values: ['day', 'hour', 'minute', 'second'],
|
||||
default: 'day',
|
||||
description:
|
||||
'Smallest unit displayed. Defaults to "day" for dates and "minute" for times.',
|
||||
},
|
||||
minValue: {
|
||||
type: 'enum',
|
||||
values: ['DateValue'],
|
||||
description: 'Minimum allowed date. Dates before this are disabled.',
|
||||
},
|
||||
maxValue: {
|
||||
type: 'enum',
|
||||
values: ['DateValue'],
|
||||
description: 'Maximum allowed date. Dates after this are disabled.',
|
||||
},
|
||||
isDateUnavailable: {
|
||||
type: 'enum',
|
||||
values: ['(date: DateValue) => boolean'],
|
||||
description:
|
||||
'Callback invoked for each calendar date. Return true to mark a date as unavailable.',
|
||||
},
|
||||
allowsNonContiguousRanges: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'When combined with isDateUnavailable, allows selecting ranges that contain unavailable dates.',
|
||||
},
|
||||
startName: {
|
||||
type: 'string',
|
||||
description: 'Form field name for the start date, submitted as ISO 8601.',
|
||||
},
|
||||
endName: {
|
||||
type: 'string',
|
||||
description: 'Form field name for the end date, submitted as ISO 8601.',
|
||||
},
|
||||
isRequired: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the field is required for form submission.',
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the picker is disabled.',
|
||||
},
|
||||
isReadOnly: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the picker is read-only.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
export const dateRangePickerUsageSnippet = `import { DateRangePicker } from '@backstage/ui';
|
||||
|
||||
<DateRangePicker label="Date range" />`;
|
||||
|
||||
export const withLabelSnippet = `<DateRangePicker
|
||||
label="Date range"
|
||||
description="Select a start and end date for your event."
|
||||
/>`;
|
||||
|
||||
export const sizesSnippet = `<Flex direction="column" gap="4">
|
||||
<DateRangePicker label="Small" size="small" />
|
||||
<DateRangePicker label="Medium" size="medium" />
|
||||
</Flex>`;
|
||||
|
||||
export const withDefaultValueSnippet = `import { parseDate } from '@internationalized/date';
|
||||
|
||||
<DateRangePicker
|
||||
label="Booking period"
|
||||
defaultValue={{
|
||||
start: parseDate('2025-02-03'),
|
||||
end: parseDate('2025-02-14'),
|
||||
}}
|
||||
/>`;
|
||||
|
||||
export const disabledSnippet = `<DateRangePicker
|
||||
label="Date range"
|
||||
isDisabled
|
||||
/>`;
|
||||
@@ -53,6 +53,10 @@ export const components: Page[] = [
|
||||
title: 'Container',
|
||||
slug: 'container',
|
||||
},
|
||||
{
|
||||
title: 'DateRangePicker',
|
||||
slug: 'date-range-picker',
|
||||
},
|
||||
{
|
||||
title: 'Dialog',
|
||||
slug: 'dialog',
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
/* ============================================================
|
||||
Root
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePicker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--bui-font-regular);
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Field group — custom container (not reusing any BUI field)
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--bui-bg-neutral-1);
|
||||
border-radius: var(--bui-radius-2);
|
||||
padding: 0 var(--bui-space-1) 0 var(--bui-space-3);
|
||||
width: fit-content;
|
||||
min-width: 280px;
|
||||
max-width: 100%;
|
||||
overflow: clip;
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
cursor: text;
|
||||
|
||||
/* bg consumer — auto-increment background based on parent context */
|
||||
&[data-on-bg='neutral-1'] {
|
||||
background-color: var(--bui-bg-neutral-2);
|
||||
}
|
||||
|
||||
&[data-on-bg='neutral-2'] {
|
||||
background-color: var(--bui-bg-neutral-3);
|
||||
}
|
||||
|
||||
&[data-on-bg='neutral-3'] {
|
||||
background-color: var(--bui-bg-neutral-4);
|
||||
}
|
||||
|
||||
&[data-focus-within] {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 1px var(--bui-ring);
|
||||
}
|
||||
|
||||
&[data-invalid] {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-danger);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
&[data-size='small'] {
|
||||
height: 2rem;
|
||||
padding-inline-start: var(--bui-space-2);
|
||||
}
|
||||
|
||||
&[data-size='medium'] {
|
||||
height: 2.5rem;
|
||||
min-width: 290px;
|
||||
padding-inline-end: var(--bui-space-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Date fields wrapper — scrollable row of inputs + separator
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerDateFields {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
overflow-x: auto;
|
||||
overflow-y: clip;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Date inputs
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerDateInput {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: unset;
|
||||
min-width: unset;
|
||||
padding: unset;
|
||||
border: unset;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
height: auto;
|
||||
|
||||
&[slot='end'] {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Date segments (month, day, year literals and placeholders)
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerSegment {
|
||||
display: inline-block;
|
||||
padding: var(--bui-space-0_5) var(--bui-space-1);
|
||||
border-radius: var(--bui-radius-1);
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
caret-color: transparent;
|
||||
outline: none;
|
||||
tabular-nums: unset;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&[data-placeholder] {
|
||||
color: var(--bui-fg-secondary);
|
||||
}
|
||||
|
||||
&[data-type='literal'] {
|
||||
color: var(--bui-fg-secondary);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&[data-focused] {
|
||||
background-color: var(--bui-bg-solid);
|
||||
color: var(--bui-fg-solid);
|
||||
border-radius: var(--bui-radius-1);
|
||||
|
||||
&[data-placeholder] {
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Separator between start and end
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerSeparator {
|
||||
color: var(--bui-fg-secondary);
|
||||
font-size: var(--bui-font-size-3);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
padding: 0 var(--bui-space-1);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Calendar trigger button
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--bui-space-1);
|
||||
border-radius: var(--bui-radius-1);
|
||||
color: var(--bui-fg-secondary);
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
transition: color 0.15s ease-in-out;
|
||||
|
||||
&[data-hovered] {
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
&[data-pressed] {
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Calendar (inside Popover)
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerCalendar {
|
||||
padding: var(--bui-space-3);
|
||||
width: fit-content;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--bui-space-3);
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarHeading {
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
font-family: var(--bui-font-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarNavButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: var(--bui-space-8);
|
||||
height: var(--bui-space-8);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--bui-radius-2);
|
||||
color: var(--bui-fg-secondary);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
|
||||
|
||||
&[data-hovered] {
|
||||
background-color: var(--bui-bg-neutral-2);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
&[data-pressed] {
|
||||
background-color: var(--bui-bg-neutral-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Calendar grid
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerCalendarGrid {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 2px;
|
||||
}
|
||||
|
||||
.bui-DateRangePickerCalendarHeaderCell {
|
||||
font-size: var(--bui-font-size-2);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-secondary);
|
||||
text-align: center;
|
||||
padding-bottom: var(--bui-space-2);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Calendar cells
|
||||
|
||||
Technique: isolation: isolate creates a stacking context so
|
||||
that ::before (range fill, z-index: -2) and ::after (circle,
|
||||
z-index: -1) paint behind the cell's text content while still
|
||||
rendering on top of the page background.
|
||||
============================================================ */
|
||||
|
||||
.bui-DateRangePickerCalendarCell {
|
||||
isolation: isolate;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
font-size: var(--bui-font-size-3);
|
||||
font-family: var(--bui-font-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
&[data-outside-month] {
|
||||
color: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
color: var(--bui-fg-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[data-unavailable] {
|
||||
color: var(--bui-fg-disabled);
|
||||
cursor: not-allowed;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&[data-hovered]:not([data-disabled]):not([data-unavailable]):not([data-selected]) {
|
||||
background-color: var(--bui-bg-neutral-2);
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: -2px;
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
/* Today marker */
|
||||
&[data-today]:not([data-selected]):not([data-selection-start]):not([data-selection-end]) {
|
||||
font-weight: var(--bui-font-weight-bold);
|
||||
}
|
||||
|
||||
/* ── Range fill via ::before ─────────────────────────────── */
|
||||
|
||||
&[data-selected]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--bui-bg-solid) 15%, transparent);
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
/* Start cell: fill only the right half to connect forward */
|
||||
&[data-selection-start]::before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
/* End cell: fill only the left half to connect backward */
|
||||
&[data-selection-end]::before {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
/* Same-day selection: no range fill needed */
|
||||
&[data-selection-start][data-selection-end]::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Solid circle for start / end via ::after ────────────── */
|
||||
|
||||
&[data-selection-start]::after,
|
||||
&[data-selection-end]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--bui-bg-solid);
|
||||
border-radius: var(--bui-radius-full);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Text color on top of the solid circle */
|
||||
&[data-selection-start],
|
||||
&[data-selection-end] {
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
}
|
||||
|
||||
/* Round the left edge of ::before when the cell is the first in its row
|
||||
(our class is on an inner element inside <td>, hence td:first-child > *) */
|
||||
.bui-DateRangePickerCalendarCell[data-selected]:not([data-selection-start]):is(
|
||||
td:first-child > *,
|
||||
[aria-disabled] + td > *
|
||||
)::before {
|
||||
border-start-start-radius: var(--bui-radius-full);
|
||||
border-end-start-radius: var(--bui-radius-full);
|
||||
}
|
||||
|
||||
/* Round the right edge of ::before when the cell is the last in its row */
|
||||
.bui-DateRangePickerCalendarCell[data-selected]:not([data-selection-end]):is(
|
||||
td:last-child > *
|
||||
)::before {
|
||||
border-start-end-radius: var(--bui-radius-full);
|
||||
border-end-end-radius: var(--bui-radius-full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import { DateRangePicker } from './DateRangePicker';
|
||||
import { parseDate, today, getLocalTimeZone } from '@internationalized/date';
|
||||
import { Form } from 'react-aria-components';
|
||||
import { Button } from '../Button';
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/DateRangePicker',
|
||||
component: DateRangePicker,
|
||||
args: {
|
||||
style: { width: 360 },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {},
|
||||
});
|
||||
|
||||
export const WithLabel = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDescription = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
description: 'Select a start and end date for your event.',
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDefaultValue = meta.story({
|
||||
args: {
|
||||
label: 'Booking period',
|
||||
defaultValue: {
|
||||
start: parseDate('2025-02-03'),
|
||||
end: parseDate('2025-02-14'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Sizes = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
},
|
||||
render: args => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
width: 360,
|
||||
}}
|
||||
>
|
||||
<DateRangePicker {...args} size="small" label="Small" />
|
||||
<DateRangePicker {...args} size="medium" label="Medium" />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export const Required = meta.story({
|
||||
args: {
|
||||
label: 'Trip dates',
|
||||
isRequired: true,
|
||||
},
|
||||
render: args => (
|
||||
<Form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
width: 360,
|
||||
}}
|
||||
>
|
||||
<DateRangePicker {...args} />
|
||||
<Button type="submit">Submit</Button>
|
||||
</Form>
|
||||
),
|
||||
});
|
||||
|
||||
export const Disabled = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
isDisabled: true,
|
||||
defaultValue: {
|
||||
start: parseDate('2025-03-01'),
|
||||
end: parseDate('2025-03-15'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Invalid = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
isInvalid: true,
|
||||
errorMessage: 'The selected range is not available.',
|
||||
defaultValue: {
|
||||
start: parseDate('2025-04-01'),
|
||||
end: parseDate('2025-04-10'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const WithMinMaxValue = meta.story({
|
||||
args: {
|
||||
label: 'Date range',
|
||||
description: 'You can only select dates within the next 30 days.',
|
||||
minValue: today(getLocalTimeZone()),
|
||||
maxValue: today(getLocalTimeZone()).add({ days: 30 }),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { forwardRef, useEffect } from 'react';
|
||||
import {
|
||||
DateRangePicker as AriaDateRangePicker,
|
||||
Popover,
|
||||
} from 'react-aria-components';
|
||||
import clsx from 'clsx';
|
||||
import { FieldLabel } from '../FieldLabel';
|
||||
import { FieldError } from '../FieldError';
|
||||
import { DateRangePickerGroup } from './DateRangePickerGroup';
|
||||
import { DateRangePickerCalendar } from './DateRangePickerCalendar';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { DateRangePickerDefinition } from './definition';
|
||||
import { PopoverDefinition } from '../Popover/definition';
|
||||
import type { DateRangePickerProps } from './types';
|
||||
|
||||
/**
|
||||
* A date range picker that combines two date fields and a calendar popover,
|
||||
* allowing users to enter or select a date range with full keyboard and
|
||||
* screen reader accessibility.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>(
|
||||
(props, ref) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
DateRangePickerDefinition,
|
||||
props,
|
||||
);
|
||||
const { ownProps: popoverOwnProps } = useDefinition(PopoverDefinition, {});
|
||||
|
||||
const { classes, label, description, secondaryLabel } = ownProps;
|
||||
|
||||
const ariaLabel = restProps['aria-label'];
|
||||
const ariaLabelledBy = restProps['aria-labelledby'];
|
||||
|
||||
useEffect(() => {
|
||||
if (!label && !ariaLabel && !ariaLabelledBy) {
|
||||
console.warn(
|
||||
'DateRangePicker requires either a visible label, aria-label, or aria-labelledby for accessibility',
|
||||
);
|
||||
}
|
||||
}, [label, ariaLabel, ariaLabelledBy]);
|
||||
|
||||
const secondaryLabelText =
|
||||
secondaryLabel || (restProps.isRequired ? 'Required' : null);
|
||||
|
||||
return (
|
||||
<AriaDateRangePicker
|
||||
className={classes.root}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
ref={ref}
|
||||
>
|
||||
<FieldLabel
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<DateRangePickerGroup dataSize={dataAttributes['data-size']} />
|
||||
<FieldError />
|
||||
<Popover className={clsx(popoverOwnProps.classes.root)}>
|
||||
<DateRangePickerCalendar />
|
||||
</Popover>
|
||||
</AriaDateRangePicker>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DateRangePicker.displayName = 'DateRangePicker';
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
RangeCalendar,
|
||||
CalendarGrid,
|
||||
CalendarGridHeader,
|
||||
CalendarHeaderCell,
|
||||
CalendarGridBody,
|
||||
CalendarCell,
|
||||
Heading,
|
||||
Button,
|
||||
} from 'react-aria-components';
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { DateRangePickerCalendarDefinition } from './definition';
|
||||
|
||||
/**
|
||||
* Calendar popover content for DateRangePicker — renders the RangeCalendar
|
||||
* with navigation and a full calendar grid.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerCalendar = () => {
|
||||
const { ownProps } = useDefinition(DateRangePickerCalendarDefinition, {});
|
||||
const { classes } = ownProps;
|
||||
|
||||
return (
|
||||
<RangeCalendar className={classes.root}>
|
||||
<header className={classes.header}>
|
||||
<Button slot="previous" className={classes.navButton}>
|
||||
<RiArrowLeftSLine size={16} />
|
||||
</Button>
|
||||
<Heading className={classes.heading} />
|
||||
<Button slot="next" className={classes.navButton}>
|
||||
<RiArrowRightSLine size={16} />
|
||||
</Button>
|
||||
</header>
|
||||
<CalendarGrid className={classes.grid}>
|
||||
<CalendarGridHeader className={classes.gridHeader}>
|
||||
{day => (
|
||||
<CalendarHeaderCell className={classes.headerCell}>
|
||||
{day}
|
||||
</CalendarHeaderCell>
|
||||
)}
|
||||
</CalendarGridHeader>
|
||||
<CalendarGridBody className={classes.gridBody}>
|
||||
{date => <CalendarCell className={classes.cell} date={date} />}
|
||||
</CalendarGridBody>
|
||||
</CalendarGrid>
|
||||
</RangeCalendar>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Group, DateInput, DateSegment, Button } from 'react-aria-components';
|
||||
import { RiCalendarLine } from '@remixicon/react';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { DateRangePickerGroupDefinition } from './definition';
|
||||
|
||||
/**
|
||||
* Custom field group for DateRangePicker — renders two DateInput fields,
|
||||
* a separator, and a calendar trigger button.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerGroup = ({ dataSize }: { dataSize?: string }) => {
|
||||
const { ownProps, dataAttributes } = useDefinition(
|
||||
DateRangePickerGroupDefinition,
|
||||
{},
|
||||
);
|
||||
const { classes } = ownProps;
|
||||
|
||||
return (
|
||||
<Group
|
||||
className={classes.root}
|
||||
{...dataAttributes}
|
||||
{...(dataSize ? { 'data-size': dataSize } : {})}
|
||||
>
|
||||
<div className={classes.dateFields}>
|
||||
<DateInput slot="start" className={classes.dateInput}>
|
||||
{segment => (
|
||||
<DateSegment segment={segment} className={classes.segment} />
|
||||
)}
|
||||
</DateInput>
|
||||
<span aria-hidden="true" className={classes.separator}>
|
||||
–
|
||||
</span>
|
||||
<DateInput slot="end" className={classes.dateInput}>
|
||||
{segment => (
|
||||
<DateSegment segment={segment} className={classes.segment} />
|
||||
)}
|
||||
</DateInput>
|
||||
</div>
|
||||
<Button className={classes.button}>
|
||||
<RiCalendarLine size={16} />
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineComponent } from '../../hooks/useDefinition';
|
||||
import type { DateRangePickerOwnProps } from './types';
|
||||
import styles from './DateRangePicker.module.css';
|
||||
|
||||
/**
|
||||
* Component definition for DateRangePicker
|
||||
* @public
|
||||
*/
|
||||
export const DateRangePickerDefinition =
|
||||
defineComponent<DateRangePickerOwnProps>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-DateRangePicker',
|
||||
},
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
className: {},
|
||||
label: {},
|
||||
description: {},
|
||||
secondaryLabel: {},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component definition for DateRangePickerGroup
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerGroupDefinition = defineComponent<
|
||||
Record<string, never>
|
||||
>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-DateRangePickerGroup',
|
||||
dateFields: 'bui-DateRangePickerDateFields',
|
||||
dateInput: 'bui-DateRangePickerDateInput',
|
||||
segment: 'bui-DateRangePickerSegment',
|
||||
separator: 'bui-DateRangePickerSeparator',
|
||||
button: 'bui-DateRangePickerButton',
|
||||
},
|
||||
bg: 'consumer',
|
||||
propDefs: {},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component definition for DateRangePickerCalendar
|
||||
* @internal
|
||||
*/
|
||||
export const DateRangePickerCalendarDefinition = defineComponent<
|
||||
Record<string, never>
|
||||
>()({
|
||||
styles,
|
||||
classNames: {
|
||||
root: 'bui-DateRangePickerCalendar',
|
||||
header: 'bui-DateRangePickerCalendarHeader',
|
||||
heading: 'bui-DateRangePickerCalendarHeading',
|
||||
navButton: 'bui-DateRangePickerCalendarNavButton',
|
||||
grid: 'bui-DateRangePickerCalendarGrid',
|
||||
gridHeader: 'bui-DateRangePickerCalendarGridHeader',
|
||||
headerCell: 'bui-DateRangePickerCalendarHeaderCell',
|
||||
gridBody: 'bui-DateRangePickerCalendarGridBody',
|
||||
cell: 'bui-DateRangePickerCalendarCell',
|
||||
},
|
||||
propDefs: {},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './DateRangePicker';
|
||||
export * from './types';
|
||||
export { DateRangePickerDefinition } from './definition';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DateRangePickerProps as AriaDateRangePickerProps } from 'react-aria-components';
|
||||
import type { DateValue } from '@internationalized/date';
|
||||
import type { Breakpoint } from '../../types';
|
||||
import type { FieldLabelProps } from '../FieldLabel/types';
|
||||
|
||||
/** @public */
|
||||
export type DateRangePickerOwnProps = {
|
||||
/**
|
||||
* The size of the date range picker
|
||||
* @defaultValue 'small'
|
||||
*/
|
||||
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
|
||||
|
||||
className?: string;
|
||||
|
||||
label?: FieldLabelProps['label'];
|
||||
description?: FieldLabelProps['description'];
|
||||
secondaryLabel?: FieldLabelProps['secondaryLabel'];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface DateRangePickerProps
|
||||
extends Omit<AriaDateRangePickerProps<DateValue>, 'className' | 'children'>,
|
||||
DateRangePickerOwnProps {}
|
||||
@@ -36,6 +36,7 @@ export { CardDefinition } from './components/Card/definition';
|
||||
export { CheckboxDefinition } from './components/Checkbox/definition';
|
||||
export { CheckboxGroupDefinition } from './components/CheckboxGroup/definition';
|
||||
export { ContainerDefinition } from './components/Container/definition';
|
||||
export { DateRangePickerDefinition } from './components/DateRangePicker/definition';
|
||||
export { DialogDefinition } from './components/Dialog/definition';
|
||||
export { FieldErrorDefinition } from './components/FieldError/definition';
|
||||
export { FieldLabelDefinition } from './components/FieldLabel/definition';
|
||||
|
||||
@@ -34,6 +34,7 @@ export * from './components/Avatar';
|
||||
export * from './components/Badge';
|
||||
export * from './components/Button';
|
||||
export * from './components/Card';
|
||||
export * from './components/DateRangePicker';
|
||||
export * from './components/Dialog';
|
||||
export * from './components/FieldLabel';
|
||||
export * from './components/PluginHeader';
|
||||
|
||||
@@ -7973,6 +7973,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@internationalized/date": "npm:^3.12.0"
|
||||
"@remixicon/react": "npm:^4.6.0"
|
||||
"@storybook/react-vite": "npm:^10.3.3"
|
||||
"@tanstack/react-table": "npm:^8.21.3"
|
||||
@@ -10127,6 +10128,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@internationalized/date@npm:^3.12.0":
|
||||
version: 3.12.0
|
||||
resolution: "@internationalized/date@npm:3.12.0"
|
||||
dependencies:
|
||||
"@swc/helpers": "npm:^0.5.0"
|
||||
checksum: 10/0bc2e95385e156362a07a8788e93b4b380b43170cca75c19a9a72cb1840c1c701bb0d1fa67d9ba5e2fbc1e44b5bea29bca02b325d0daa2f960b92a71898ede92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@internationalized/date@npm:^3.12.1":
|
||||
version: 3.12.1
|
||||
resolution: "@internationalized/date@npm:3.12.1"
|
||||
@@ -11885,8 +11895,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@mswjs/interceptors@npm:^0.39.1":
|
||||
version: 0.39.8
|
||||
resolution: "@mswjs/interceptors@npm:0.39.8"
|
||||
version: 0.39.6
|
||||
resolution: "@mswjs/interceptors@npm:0.39.6"
|
||||
dependencies:
|
||||
"@open-draft/deferred-promise": "npm:^2.2.0"
|
||||
"@open-draft/logger": "npm:^0.3.0"
|
||||
@@ -11894,7 +11904,7 @@ __metadata:
|
||||
is-node-process: "npm:^1.2.0"
|
||||
outvariant: "npm:^1.4.3"
|
||||
strict-event-emitter: "npm:^0.5.1"
|
||||
checksum: 10/d92546cf9bf670ddb927c53f5fa19f0554b7475a264ead4e1ae2339874f4312fe4ada5d42588f27eea3577bee29fa8f46889d398f0e7ecb3f7a4c1d3e0b71bdc
|
||||
checksum: 10/c87d3edf08353bde825c87b151b24d538070540ab419206cef1774c932e888af0f920183182fb7c94c3eee42068da5a0a5855853fded8514f33c870921ef37ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -27474,14 +27484,14 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"dompurify@npm:^3.1.7, dompurify@npm:^3.3.2":
|
||||
version: 3.4.0
|
||||
resolution: "dompurify@npm:3.4.0"
|
||||
version: 3.3.3
|
||||
resolution: "dompurify@npm:3.3.3"
|
||||
dependencies:
|
||||
"@types/trusted-types": "npm:^2.0.7"
|
||||
dependenciesMeta:
|
||||
"@types/trusted-types":
|
||||
optional: true
|
||||
checksum: 10/ead40b78ec51cd451f2c74fada4233ee0afeafdbab54af2f4a4bd5d4d138ac04d0d85140e79f533803ecfd1c3758edc1176087039c1e7217824f9794a9d34d2c
|
||||
checksum: 10/4cc9c539ed7136d46c6577613b8e20871c2b6165db01dfbd2a3c11c75f9e339c496ac6519a1c3190115def8cadae3720bef0417fc43fa28802c7407bab174da9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -47215,7 +47225,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici@npm:7.24.7, undici@npm:^7.24.3, undici@npm:^7.24.5":
|
||||
"undici@npm:7.24.7":
|
||||
version: 7.24.7
|
||||
resolution: "undici@npm:7.24.7"
|
||||
checksum: 10/bce7b75fe2656bbd1f9c9d5d1b6b89670773281343be25d0b1f4d808dcce97d81509987d1f3183d37a63d3a57f5f217ed8ed15ee3e103384c54e190f4e360c48
|
||||
@@ -47231,6 +47241,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici@npm:^7.24.3, undici@npm:^7.24.5":
|
||||
version: 7.24.6
|
||||
resolution: "undici@npm:7.24.6"
|
||||
checksum: 10/9c8df674284b1e9b8c3fee543883ad71c20d5fb3b6f6595330342f3dad30de944c2d55ff15aa59e7a088af272ce73be6e93baad9ab817b1b82e5c83298c23d8e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uni-global@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "uni-global@npm:1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user