Compare commits
1 Commits
v0.1.18
...
3c7534ef11
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c7534ef11 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@panic/web-ui",
|
"name": "@panic/web-ui",
|
||||||
"version": "0.1.18",
|
"version": "0.1.17",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"description": "Core components for panic.haus web applications",
|
"description": "Core components for panic.haus web applications",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -90,8 +90,5 @@
|
|||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
"vitest": "^4.0.18",
|
"vitest": "^4.0.18",
|
||||||
"yjs": "^13.6.24"
|
"yjs": "^13.6.24"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "^25.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const meta = {
|
|||||||
docs: {
|
docs: {
|
||||||
description: {
|
description: {
|
||||||
component:
|
component:
|
||||||
'In-house date/time selection field with InputField-compatible API. Uses a custom popup (not native browser pickers) and supports date, time, and date-time modes.',
|
'Date selection field with InputField-compatible API, supporting date/time/datetime-local values, size/layout variants, and validation state.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -27,10 +27,10 @@ const meta = {
|
|||||||
table: { type: { summary: 'string' } },
|
table: { type: { summary: 'string' } },
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
description: 'DatePicker mode.',
|
description: 'Native date input type.',
|
||||||
options: ['date', 'date-time', 'time'],
|
options: ['date', 'datetime-local', 'time'],
|
||||||
control: 'inline-radio',
|
control: 'inline-radio',
|
||||||
table: { type: { summary: "'date' | 'date-time' | 'time'" } },
|
table: { type: { summary: "'date' | 'datetime-local' | 'time'" } },
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
description: 'Input size.',
|
description: 'Input size.',
|
||||||
@@ -55,22 +55,6 @@ const meta = {
|
|||||||
control: 'text',
|
control: 'text',
|
||||||
table: { type: { summary: 'string' } },
|
table: { type: { summary: 'string' } },
|
||||||
},
|
},
|
||||||
format: {
|
|
||||||
description:
|
|
||||||
'Optional input/output format. Supported tokens: `dd`, `mm`, `yyyy`, `HH` (for example `dd/mm/yyyy HH:mm`).',
|
|
||||||
control: 'text',
|
|
||||||
table: { type: { summary: 'string' } },
|
|
||||||
},
|
|
||||||
min: {
|
|
||||||
description: 'Optional minimum value in the same format as `value`.',
|
|
||||||
control: 'text',
|
|
||||||
table: { type: { summary: 'string' } },
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
description: 'Optional maximum value in the same format as `value`.',
|
|
||||||
control: 'text',
|
|
||||||
table: { type: { summary: 'string' } },
|
|
||||||
},
|
|
||||||
name: {
|
name: {
|
||||||
description: 'Native input `name` attribute.',
|
description: 'Native input `name` attribute.',
|
||||||
control: 'text',
|
control: 'text',
|
||||||
@@ -124,7 +108,7 @@ const meta = {
|
|||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
label: 'Schedule at',
|
label: 'Schedule at',
|
||||||
type: 'date-time',
|
type: 'datetime-local',
|
||||||
value: '',
|
value: '',
|
||||||
size: 'md',
|
size: 'md',
|
||||||
width: 'md',
|
width: 'md',
|
||||||
@@ -140,8 +124,8 @@ export const DateOnly: Story = {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
label: 'Publish date',
|
label: 'Publish date',
|
||||||
},
|
},
|
||||||
render: function DateOnlyRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('2031/05/20');
|
const [value, setValue] = useState('2031-05-20');
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...args}
|
{...args}
|
||||||
@@ -157,11 +141,11 @@ export const DateOnly: Story = {
|
|||||||
|
|
||||||
export const DateTime: Story = {
|
export const DateTime: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: 'date-time',
|
type: 'datetime-local',
|
||||||
label: 'Schedule at',
|
label: 'Schedule at',
|
||||||
},
|
},
|
||||||
render: function DateTimeRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('2031/05/20 14:30');
|
const [value, setValue] = useState('2031-05-20T14:30');
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...args}
|
{...args}
|
||||||
@@ -183,7 +167,7 @@ export const TimeOnlyInline: Story = {
|
|||||||
size: 'sm',
|
size: 'sm',
|
||||||
rightIcon: <CalendarDaysIcon className="h-4 w-4 ui-body-secondary" />,
|
rightIcon: <CalendarDaysIcon className="h-4 w-4 ui-body-secondary" />,
|
||||||
},
|
},
|
||||||
render: function TimeOnlyInlineRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('09:00');
|
const [value, setValue] = useState('09:00');
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -198,10 +182,9 @@ export const TimeOnlyInline: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ErrorState: Story = {
|
export const Error: Story = {
|
||||||
name: 'Error',
|
|
||||||
args: {
|
args: {
|
||||||
type: 'date-time',
|
type: 'datetime-local',
|
||||||
label: 'Schedule at',
|
label: 'Schedule at',
|
||||||
value: '',
|
value: '',
|
||||||
error: 'Pick a valid future date and time',
|
error: 'Pick a valid future date and time',
|
||||||
@@ -210,20 +193,20 @@ export const ErrorState: Story = {
|
|||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: 'date-time',
|
type: 'datetime-local',
|
||||||
label: 'Published at',
|
label: 'Published at',
|
||||||
value: '2031/05/20 14:30',
|
value: '2031-05-20T14:30',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SizeMatrix: Story = {
|
export const SizeMatrix: Story = {
|
||||||
args: {
|
args: {
|
||||||
type: 'date-time',
|
type: 'datetime-local',
|
||||||
label: 'Schedule at',
|
label: 'Schedule at',
|
||||||
},
|
},
|
||||||
render: function SizeMatrixRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('2031/05/20 14:30');
|
const [value, setValue] = useState('2031-05-20T14:30');
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
@@ -255,26 +238,3 @@ export const SizeMatrix: Story = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomFormatWithRange: Story = {
|
|
||||||
args: {
|
|
||||||
type: 'date-time',
|
|
||||||
label: 'Starts at',
|
|
||||||
format: 'dd/mm/yyyy HH:mm',
|
|
||||||
min: '10/03/2026 09:00',
|
|
||||||
max: '24/03/2026 18:30',
|
|
||||||
},
|
|
||||||
render: function CustomFormatWithRangeRender(args) {
|
|
||||||
const [value, setValue] = useState('22/03/2026 14:30');
|
|
||||||
return (
|
|
||||||
<DatePicker
|
|
||||||
{...args}
|
|
||||||
value={value}
|
|
||||||
onChange={(event) => {
|
|
||||||
setValue(event.target.value);
|
|
||||||
args.onChange?.(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -99,7 +99,7 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Stacked: Story = {
|
export const Stacked: Story = {
|
||||||
render: function StackedRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState(args.value);
|
const [value, setValue] = useState(args.value);
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -119,7 +119,7 @@ export const Inline: Story = {
|
|||||||
layout: 'inline',
|
layout: 'inline',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
},
|
},
|
||||||
render: function InlineRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState(args.value);
|
const [value, setValue] = useState(args.value);
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -147,7 +147,7 @@ export const WithError: Story = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SizeMatrix: Story = {
|
export const SizeMatrix: Story = {
|
||||||
render: function SizeMatrixRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState(args.value);
|
const [value, setValue] = useState(args.value);
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const Basic: Story = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const WithActions: Story = {
|
export const WithActions: Story = {
|
||||||
render: function WithActionsRender(args) {
|
render: (args) => {
|
||||||
const [title, setTitle] = useState('Storybook powered CMS');
|
const [title, setTitle] = useState('Storybook powered CMS');
|
||||||
const [status, setStatus] = useState('draft');
|
const [status, setStatus] = useState('draft');
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export const Text: Story = {
|
|||||||
label: 'Title',
|
label: 'Title',
|
||||||
placeholder: 'Write a title',
|
placeholder: 'Write a title',
|
||||||
},
|
},
|
||||||
render: function TextRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('Storybook integration');
|
const [value, setValue] = useState('Storybook integration');
|
||||||
return (
|
return (
|
||||||
<InputField
|
<InputField
|
||||||
@@ -148,7 +148,7 @@ export const PasswordWithToggle: Story = {
|
|||||||
label: 'Password',
|
label: 'Password',
|
||||||
placeholder: 'Type a strong password',
|
placeholder: 'Type a strong password',
|
||||||
},
|
},
|
||||||
render: function PasswordWithToggleRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('pa55word');
|
const [value, setValue] = useState('pa55word');
|
||||||
return (
|
return (
|
||||||
<InputField
|
<InputField
|
||||||
@@ -171,7 +171,7 @@ export const InlineWithIcon: Story = {
|
|||||||
size: 'sm',
|
size: 'sm',
|
||||||
rightIcon: <MagnifyingGlassIcon className="h-4 w-4 ui-body-secondary" />,
|
rightIcon: <MagnifyingGlassIcon className="h-4 w-4 ui-body-secondary" />,
|
||||||
},
|
},
|
||||||
render: function InlineWithIconRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('posts');
|
const [value, setValue] = useState('posts');
|
||||||
return (
|
return (
|
||||||
<InputField
|
<InputField
|
||||||
@@ -186,8 +186,7 @@ export const InlineWithIcon: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ErrorState: Story = {
|
export const Error: Story = {
|
||||||
name: 'Error',
|
|
||||||
args: {
|
args: {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
@@ -211,7 +210,7 @@ export const SizeMatrix: Story = {
|
|||||||
label: 'Name',
|
label: 'Name',
|
||||||
placeholder: 'Enter value',
|
placeholder: 'Enter value',
|
||||||
},
|
},
|
||||||
render: function SizeMatrixRender(args) {
|
render: (args) => {
|
||||||
const [value, setValue] = useState('Beatrice');
|
const [value, setValue] = useState('Beatrice');
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const Body: Story = {};
|
export const Body: Story = {};
|
||||||
|
|
||||||
export const ErrorState: Story = {
|
export const Error: Story = {
|
||||||
name: 'Error',
|
|
||||||
args: {
|
args: {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
children: 'This field is required',
|
children: 'This field is required',
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Editable: Story = {
|
export const Editable: Story = {
|
||||||
render: function EditableRender(args) {
|
render: (args) => {
|
||||||
const [markdown, setMarkdown] = useState(args.markdown);
|
const [markdown, setMarkdown] = useState(args.markdown);
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export const Empty: Story = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const InteractiveSortingAndPagination: Story = {
|
export const InteractiveSortingAndPagination: Story = {
|
||||||
render: function InteractiveSortingAndPaginationRender() {
|
render: () => {
|
||||||
const [sorting, setSorting] = useState<SortState | null>({
|
const [sorting, setSorting] = useState<SortState | null>({
|
||||||
field: 'name',
|
field: 'name',
|
||||||
direction: 'asc',
|
direction: 'asc',
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
--bg-page: #16121a;
|
--bg-page: #16121a;
|
||||||
--surface-bg: rgba(24, 24, 27, 0.45);
|
--surface-bg: rgba(24, 24, 27, 0.45);
|
||||||
--surface-bg-strong: rgba(24, 24, 27, 0.62);
|
--surface-bg-strong: rgba(24, 24, 27, 0.62);
|
||||||
--datepicker-menu-bg: #18181b;
|
|
||||||
--surface-border: rgba(82, 82, 91, 0.6);
|
--surface-border: rgba(82, 82, 91, 0.6);
|
||||||
--surface-divider: rgba(63, 63, 70, 0.85);
|
--surface-divider: rgba(63, 63, 70, 0.85);
|
||||||
--text-primary: #d5cfdf;
|
--text-primary: #d5cfdf;
|
||||||
@@ -60,7 +59,6 @@
|
|||||||
--bg-page: #f7f7fb;
|
--bg-page: #f7f7fb;
|
||||||
--surface-bg: rgba(255, 255, 255, 0.9);
|
--surface-bg: rgba(255, 255, 255, 0.9);
|
||||||
--surface-bg-strong: rgba(255, 255, 255, 0.98);
|
--surface-bg-strong: rgba(255, 255, 255, 0.98);
|
||||||
--datepicker-menu-bg: #ffffff;
|
|
||||||
--surface-border: rgba(161, 161, 170, 0.45);
|
--surface-border: rgba(161, 161, 170, 0.45);
|
||||||
--surface-divider: rgba(212, 212, 216, 0.9);
|
--surface-divider: rgba(212, 212, 216, 0.9);
|
||||||
--text-primary: #52485c;
|
--text-primary: #52485c;
|
||||||
|
|||||||
@@ -327,190 +327,3 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@apply px-4 py-3 text-sm;
|
@apply px-4 py-3 text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datepicker-icon-btn {
|
|
||||||
color: var(--text-muted);
|
|
||||||
@apply absolute inset-y-0 right-2 my-auto inline-flex h-6 w-6 items-center justify-center rounded-md border border-transparent bg-transparent p-0 transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-icon-btn:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background-color: var(--ghost-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-icon-btn:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
border-color: rgb(var(--accent-400));
|
|
||||||
box-shadow: 0 0 0 2px rgb(var(--accent-400) / 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-icon-btn:disabled {
|
|
||||||
color: var(--ghost-disabled-text);
|
|
||||||
background-color: transparent;
|
|
||||||
@apply cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-popup {
|
|
||||||
border: 1px solid var(--surface-divider);
|
|
||||||
background-color: var(--surface-bg-strong);
|
|
||||||
box-shadow: var(--shadow-glow);
|
|
||||||
color: var(--text-primary);
|
|
||||||
backdrop-filter: saturate(145%) blur(var(--auth-glass-blur));
|
|
||||||
-webkit-backdrop-filter: saturate(145%) blur(var(--auth-glass-blur));
|
|
||||||
will-change: backdrop-filter;
|
|
||||||
max-width: min(96vw, 440px);
|
|
||||||
@apply fixed z-[70] flex flex-col gap-3 rounded-xl p-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-popup-top {
|
|
||||||
transform-origin: bottom center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-popup-bottom {
|
|
||||||
transform-origin: top center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen sm {
|
|
||||||
.datepicker-popup {
|
|
||||||
@apply flex-row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-panel {
|
|
||||||
@apply min-w-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-calendar-nav {
|
|
||||||
@apply mb-2 flex items-center justify-between gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-nav-btn {
|
|
||||||
border: 1px solid var(--ghost-border);
|
|
||||||
background-color: var(--ghost-bg);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
@apply inline-flex h-8 w-8 items-center justify-center rounded-lg transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-nav-btn:hover {
|
|
||||||
background-color: var(--ghost-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-heading-controls {
|
|
||||||
@apply relative flex items-center gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-chooser {
|
|
||||||
@apply relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-chooser-btn {
|
|
||||||
border: 1px solid var(--field-border);
|
|
||||||
background-color: var(--field-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
@apply inline-flex h-8 items-center rounded-lg px-2.5 text-xs font-semibold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-chooser-menu {
|
|
||||||
border: 1px solid var(--surface-divider);
|
|
||||||
background-color: var(--datepicker-menu-bg);
|
|
||||||
box-shadow: var(--shadow-glow);
|
|
||||||
@apply absolute left-0 top-full z-20 mt-1 max-h-48 min-w-[9rem] overflow-y-auto rounded-lg p-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-chooser-option {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
@apply block w-full rounded-md px-2 py-1.5 text-left text-xs font-medium transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-chooser-option:hover {
|
|
||||||
background-color: var(--ghost-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-chooser-option.is-selected {
|
|
||||||
background-color: rgb(var(--accent-500) / 0.22);
|
|
||||||
color: rgb(var(--accent-300));
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-weekdays {
|
|
||||||
@apply mb-1 grid grid-cols-7 gap-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-weekday {
|
|
||||||
color: var(--text-muted);
|
|
||||||
@apply text-center text-[0.65rem] font-semibold uppercase tracking-[0.08em];
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-grid {
|
|
||||||
@apply grid grid-cols-7 gap-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-day {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
@apply inline-flex h-8 w-8 items-center justify-center rounded-lg text-sm transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-day:hover {
|
|
||||||
background-color: var(--ghost-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-day.is-selected {
|
|
||||||
background-color: rgb(var(--accent-500));
|
|
||||||
color: rgb(var(--accent-contrast));
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-day.is-today {
|
|
||||||
border: 1px solid rgb(var(--accent-400) / 0.65);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-day.is-outside-month {
|
|
||||||
color: var(--text-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-day:disabled {
|
|
||||||
color: var(--ghost-disabled-text);
|
|
||||||
background-color: transparent;
|
|
||||||
@apply cursor-not-allowed opacity-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-root {
|
|
||||||
@apply grid min-w-[180px] grid-cols-2 gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-column {
|
|
||||||
@apply flex min-w-0 flex-col gap-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-title {
|
|
||||||
color: var(--text-muted);
|
|
||||||
@apply text-[0.65rem] font-semibold uppercase tracking-[0.08em];
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-list {
|
|
||||||
border: 1px solid var(--field-border);
|
|
||||||
background-color: var(--field-bg);
|
|
||||||
@apply max-h-52 overflow-y-auto rounded-lg p-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-option {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
@apply block w-full rounded-md px-2 py-1.5 text-left text-xs font-semibold transition;
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-option:hover {
|
|
||||||
background-color: var(--ghost-hover);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-option.is-selected {
|
|
||||||
background-color: rgb(var(--accent-500) / 0.22);
|
|
||||||
color: rgb(var(--accent-300));
|
|
||||||
}
|
|
||||||
|
|
||||||
.datepicker-time-option:disabled {
|
|
||||||
color: var(--ghost-disabled-text);
|
|
||||||
background-color: transparent;
|
|
||||||
@apply cursor-not-allowed opacity-55;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,334 +1,41 @@
|
|||||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { DatePicker } from '../../src/components/DatePicker';
|
import { DatePicker } from '../../src/components/DatePicker';
|
||||||
|
|
||||||
type ControlledProps = {
|
|
||||||
type: 'date' | 'time' | 'date-time';
|
|
||||||
initialValue: string;
|
|
||||||
format?: string;
|
|
||||||
min?: string;
|
|
||||||
max?: string;
|
|
||||||
onValueChange?: (value: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function ControlledDatePicker({
|
|
||||||
type,
|
|
||||||
initialValue,
|
|
||||||
format,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
onValueChange,
|
|
||||||
disabled = false,
|
|
||||||
}: Readonly<ControlledProps>) {
|
|
||||||
const [value, setValue] = useState(initialValue);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DatePicker
|
|
||||||
label="Schedule"
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
format={format}
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setValue(event.target.value);
|
|
||||||
onValueChange?.(event.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickCurrentMonthDay(label: string): void {
|
|
||||||
const dayButtons = Array.from(document.querySelectorAll('.datepicker-day')) as HTMLButtonElement[];
|
|
||||||
const targetButton = dayButtons.find(
|
|
||||||
(button) => button.textContent === label && !button.classList.contains('is-outside-month'),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(targetButton).toBeDefined();
|
|
||||||
fireEvent.click(targetButton as HTMLButtonElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentMonthDayButton(label: string): HTMLButtonElement {
|
|
||||||
const dayButtons = Array.from(document.querySelectorAll('.datepicker-day')) as HTMLButtonElement[];
|
|
||||||
const targetButton = dayButtons.find(
|
|
||||||
(button) => button.textContent === label && !button.classList.contains('is-outside-month'),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!targetButton) {
|
|
||||||
throw new Error(`Could not find day button for ${label}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DatePicker', () => {
|
describe('DatePicker', () => {
|
||||||
it('opens popup from icon button and closes with Escape', () => {
|
it('supports datetime-local type and change callback', () => {
|
||||||
render(<DatePicker label="Schedule" type="date-time" value="2031/05/20 14:30" onChange={() => {}} />);
|
const onChange = vi.fn();
|
||||||
|
render(<DatePicker label="Schedule" type="datetime-local" value="" onChange={onChange} />);
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
||||||
expect(input.type).toBe('text');
|
expect(input.type).toBe('datetime-local');
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
fireEvent.change(input, { target: { value: '2031-05-20T14:30' } });
|
||||||
expect(
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
screen.getByRole('dialog', { name: 'Date and time picker popup' }),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.keyDown(document, { key: 'Escape' });
|
|
||||||
expect(
|
|
||||||
screen.queryByRole('dialog', { name: 'Date and time picker popup' }),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports segment-by-segment editing with custom format and auto-advance', async () => {
|
it('supports date type and disabled state', () => {
|
||||||
const onValueChange = vi.fn();
|
|
||||||
render(
|
render(
|
||||||
<ControlledDatePicker
|
<DatePicker
|
||||||
type="date-time"
|
label="Publish date"
|
||||||
format="dd/mm/yyyy HH:mm"
|
|
||||||
initialValue="22/02/2026 14:30"
|
|
||||||
onValueChange={onValueChange}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
|
||||||
fireEvent.focus(input);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(0);
|
|
||||||
expect(input.selectionEnd).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '1' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('01/02/2026 14:30');
|
|
||||||
expect(input.selectionStart).toBe(0);
|
|
||||||
expect(input.selectionEnd).toBe(2);
|
|
||||||
});
|
|
||||||
fireEvent.keyDown(input, { key: '1' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('11/02/2026 14:30');
|
|
||||||
expect(input.selectionStart).toBe(3);
|
|
||||||
expect(input.selectionEnd).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '0' });
|
|
||||||
fireEvent.keyDown(input, { key: '3' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('11/03/2026 14:30');
|
|
||||||
expect(input.selectionStart).toBe(6);
|
|
||||||
expect(input.selectionEnd).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '2' });
|
|
||||||
fireEvent.keyDown(input, { key: '0' });
|
|
||||||
fireEvent.keyDown(input, { key: '2' });
|
|
||||||
fireEvent.keyDown(input, { key: '7' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('11/03/2027 14:30');
|
|
||||||
expect(input.selectionStart).toBe(11);
|
|
||||||
expect(input.selectionEnd).toBe(13);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '1' });
|
|
||||||
fireEvent.keyDown(input, { key: '6' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('11/03/2027 16:30');
|
|
||||||
expect(input.selectionStart).toBe(14);
|
|
||||||
expect(input.selectionEnd).toBe(16);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '4' });
|
|
||||||
fireEvent.keyDown(input, { key: '5' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('11/03/2027 16:45');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(onValueChange).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves year 0000 while editing and does not coerce to 19xx', async () => {
|
|
||||||
render(
|
|
||||||
<ControlledDatePicker
|
|
||||||
type="date-time"
|
|
||||||
format="dd/mm/yyyy HH:mm"
|
|
||||||
initialValue="22/02/2026 14:30"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
|
||||||
fireEvent.focus(input);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(0);
|
|
||||||
expect(input.selectionEnd).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '/' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(3);
|
|
||||||
expect(input.selectionEnd).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '/' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(6);
|
|
||||||
expect(input.selectionEnd).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '0' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('22/02/0000 14:30');
|
|
||||||
expect(input.selectionStart).toBe(6);
|
|
||||||
expect(input.selectionEnd).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '0' });
|
|
||||||
fireEvent.keyDown(input, { key: '0' });
|
|
||||||
fireEvent.keyDown(input, { key: '0' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.value).toBe('22/02/0000 14:30');
|
|
||||||
expect(input.selectionStart).toBe(11);
|
|
||||||
expect(input.selectionEnd).toBe(13);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses separator keys to move between editable segments', async () => {
|
|
||||||
render(
|
|
||||||
<ControlledDatePicker
|
|
||||||
type="date-time"
|
|
||||||
format="dd/mm/yyyy HH:mm"
|
|
||||||
initialValue="22/02/2026 14:30"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
|
||||||
fireEvent.focus(input);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(0);
|
|
||||||
expect(input.selectionEnd).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '/' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(3);
|
|
||||||
expect(input.selectionEnd).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: '/' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(6);
|
|
||||||
expect(input.selectionEnd).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: ' ' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(11);
|
|
||||||
expect(input.selectionEnd).toBe(13);
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.keyDown(input, { key: ':' });
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(input.selectionStart).toBe(14);
|
|
||||||
expect(input.selectionEnd).toBe(16);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies min/max constraints to calendar and time options', () => {
|
|
||||||
render(
|
|
||||||
<ControlledDatePicker
|
|
||||||
type="date-time"
|
|
||||||
initialValue="2026/03/10 10:00"
|
|
||||||
min="2026/03/10 09:30"
|
|
||||||
max="2026/03/10 10:15"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
||||||
|
|
||||||
expect(getCurrentMonthDayButton('9')).toBeDisabled();
|
|
||||||
expect(getCurrentMonthDayButton('10')).toBeEnabled();
|
|
||||||
expect(getCurrentMonthDayButton('11')).toBeDisabled();
|
|
||||||
|
|
||||||
const hoursList = screen.getByRole('listbox', { name: 'Hours' });
|
|
||||||
expect(within(hoursList).getByRole('button', { name: '08' })).toBeDisabled();
|
|
||||||
expect(within(hoursList).getByRole('button', { name: '09' })).toBeEnabled();
|
|
||||||
expect(within(hoursList).getByRole('button', { name: '10' })).toBeEnabled();
|
|
||||||
expect(within(hoursList).getByRole('button', { name: '11' })).toBeDisabled();
|
|
||||||
|
|
||||||
const minutesList = screen.getByRole('listbox', { name: 'Minutes' });
|
|
||||||
expect(within(minutesList).getByRole('button', { name: '15' })).toBeEnabled();
|
|
||||||
expect(within(minutesList).getByRole('button', { name: '20' })).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders date mode calendar only and auto-closes after day selection', () => {
|
|
||||||
const onValueChange = vi.fn();
|
|
||||||
render(
|
|
||||||
<ControlledDatePicker
|
|
||||||
type="date"
|
type="date"
|
||||||
initialValue="2031/05/20"
|
value="2031-05-20"
|
||||||
onValueChange={onValueChange}
|
onChange={() => {}}
|
||||||
|
disabled
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
const input = screen.getByLabelText('Publish date') as HTMLInputElement;
|
||||||
|
expect(input.type).toBe('date');
|
||||||
expect(screen.getByRole('dialog', { name: 'Date picker popup' })).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Hours')).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Minutes')).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
pickCurrentMonthDay('15');
|
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
|
||||||
expect(input.value).toMatch(/^\d{4}\/\d{2}\/15$/);
|
|
||||||
expect(onValueChange).toHaveBeenCalled();
|
|
||||||
expect(screen.queryByRole('dialog', { name: 'Date picker popup' })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders time mode selectors only and keeps popup open after selection', () => {
|
|
||||||
const onValueChange = vi.fn();
|
|
||||||
render(<ControlledDatePicker type="time" initialValue="09:00" onValueChange={onValueChange} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Open date picker' }));
|
|
||||||
|
|
||||||
expect(screen.getByRole('dialog', { name: 'Time picker popup' })).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
const hoursList = screen.getByRole('listbox', { name: 'Hours' });
|
|
||||||
const minutesList = screen.getByRole('listbox', { name: 'Minutes' });
|
|
||||||
|
|
||||||
fireEvent.click(within(hoursList).getByRole('button', { name: '13' }));
|
|
||||||
fireEvent.click(within(minutesList).getByRole('button', { name: '45' }));
|
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
|
||||||
expect(input.value).toBe('13:45');
|
|
||||||
expect(onValueChange).toHaveBeenCalledTimes(2);
|
|
||||||
expect(screen.getByRole('dialog', { name: 'Time picker popup' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('blocks popup interactions while disabled', () => {
|
|
||||||
render(<ControlledDatePicker type="date" initialValue="2031/05/20" disabled />);
|
|
||||||
|
|
||||||
const input = screen.getByLabelText('Schedule') as HTMLInputElement;
|
|
||||||
const iconButton = screen.getByRole('button', { name: 'Open date picker' });
|
|
||||||
|
|
||||||
expect(input).toBeDisabled();
|
expect(input).toBeDisabled();
|
||||||
expect(iconButton).toBeDisabled();
|
|
||||||
|
|
||||||
fireEvent.click(iconButton);
|
|
||||||
|
|
||||||
expect(screen.queryByRole('dialog', { name: 'Date picker popup' })).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders right icon and error message', () => {
|
it('renders right icon and error message', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Schedule"
|
label="Schedule"
|
||||||
type="date-time"
|
type="datetime-local"
|
||||||
value="2031/05/20 14:30"
|
value=""
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
rightIcon={<span data-testid="right-icon">R</span>}
|
rightIcon={<span data-testid="right-icon">R</span>}
|
||||||
error="Invalid date"
|
error="Invalid date"
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ vi.mock('@mdxeditor/editor', () => import('./mocks/mdxeditor'));
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
const storage = globalThis.window?.localStorage ?? globalThis.localStorage;
|
localStorage.clear();
|
||||||
if (typeof storage?.clear === 'function') {
|
|
||||||
storage.clear();
|
|
||||||
}
|
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,17 +34,15 @@ export default defineConfig({
|
|||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
include: ['src/**/*.{ts,tsx}'],
|
include: ['src/**/*.{ts,tsx}'],
|
||||||
exclude: [
|
exclude: [
|
||||||
'src/**/*.story.{ts,tsx}',
|
|
||||||
'src/**/*.stories.{ts,tsx}',
|
'src/**/*.stories.{ts,tsx}',
|
||||||
'src/index.ts',
|
|
||||||
'src/styles/**',
|
'src/styles/**',
|
||||||
'src/components/types.ts',
|
'src/components/types.ts',
|
||||||
'src/types/**',
|
'src/types/**',
|
||||||
],
|
],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
lines: 80,
|
lines: 95,
|
||||||
functions: 75,
|
functions: 95,
|
||||||
branches: 70,
|
branches: 90,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
28
yarn.lock
28
yarn.lock
@@ -2118,13 +2118,6 @@
|
|||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
|
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
|
||||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||||
|
|
||||||
"@types/node@^25.3.0":
|
|
||||||
version "25.3.0"
|
|
||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/node/-/node-25.3.0.tgz#749b1bd4058e51b72e22bd41e9eab6ebd0180470"
|
|
||||||
integrity sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==
|
|
||||||
dependencies:
|
|
||||||
undici-types "~7.18.0"
|
|
||||||
|
|
||||||
"@types/react-dom@^19.0.0":
|
"@types/react-dom@^19.0.0":
|
||||||
version "19.2.3"
|
version "19.2.3"
|
||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c"
|
resolved "https://nexus.beatrice.wtf/repository/npm-group/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c"
|
||||||
@@ -4545,16 +4538,16 @@ react-remove-scroll@^2.6.3:
|
|||||||
use-sidecar "^1.1.3"
|
use-sidecar "^1.1.3"
|
||||||
|
|
||||||
react-router-dom@^7.0.0:
|
react-router-dom@^7.0.0:
|
||||||
version "7.13.0"
|
version "7.13.1"
|
||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/react-router-dom/-/react-router-dom-7.13.0.tgz#8b5f7204fadca680f0e94f207c163f0dcd1cfdf5"
|
resolved "https://nexus.beatrice.wtf/repository/npm-group/react-router-dom/-/react-router-dom-7.13.1.tgz#74c045acc333ca94612b889cd1b1e1ee9534dead"
|
||||||
integrity sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==
|
integrity sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
react-router "7.13.0"
|
react-router "7.13.1"
|
||||||
|
|
||||||
react-router@7.13.0:
|
react-router@7.13.1:
|
||||||
version "7.13.0"
|
version "7.13.1"
|
||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/react-router/-/react-router-7.13.0.tgz#de9484aee764f4f65b93275836ff5944d7f5bd3b"
|
resolved "https://nexus.beatrice.wtf/repository/npm-group/react-router/-/react-router-7.13.1.tgz#5e2b3ebafd6c78d9775e135474bf5060645077f7"
|
||||||
integrity sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==
|
integrity sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie "^1.0.1"
|
cookie "^1.0.1"
|
||||||
set-cookie-parser "^2.6.0"
|
set-cookie-parser "^2.6.0"
|
||||||
@@ -5008,11 +5001,6 @@ typescript@^5.6.2:
|
|||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
resolved "https://nexus.beatrice.wtf/repository/npm-group/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||||
|
|
||||||
undici-types@~7.18.0:
|
|
||||||
version "7.18.2"
|
|
||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9"
|
|
||||||
integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==
|
|
||||||
|
|
||||||
undici@^7.21.0:
|
undici@^7.21.0:
|
||||||
version "7.22.0"
|
version "7.22.0"
|
||||||
resolved "https://nexus.beatrice.wtf/repository/npm-group/undici/-/undici-7.22.0.tgz#7a82590a5908e504a47d85c60b0f89ca14240e60"
|
resolved "https://nexus.beatrice.wtf/repository/npm-group/undici/-/undici-7.22.0.tgz#7a82590a5908e504a47d85c60b0f89ca14240e60"
|
||||||
|
|||||||
Reference in New Issue
Block a user