@altinn/altinn-components 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/lib/components/Bookmarks/BookmarksList.tsx +17 -0
- package/lib/components/Bookmarks/BookmarksListItem.stories.tsx +79 -0
- package/lib/components/Bookmarks/BookmarksListItem.tsx +32 -0
- package/lib/components/Bookmarks/QueryLabel.tsx +33 -0
- package/lib/components/Bookmarks/index.ts +3 -0
- package/lib/components/Bookmarks/queryLabel.module.css +45 -0
- package/lib/components/Button/ButtonBase.tsx +2 -1
- package/lib/components/Button/IconButton.tsx +3 -1
- package/lib/components/ContextMenu/ContextMenuBase.tsx +3 -1
- package/lib/components/Datepicker/Datepicker.tsx +49 -0
- package/lib/components/Datepicker/DatepickerBase.tsx +12 -0
- package/lib/components/Datepicker/DatepickerHeader.tsx +20 -0
- package/lib/components/Datepicker/DatepickerTable.tsx +50 -0
- package/lib/components/Datepicker/datepickerBase.module.css +5 -0
- package/lib/components/Datepicker/datepickerHeader.module.css +17 -0
- package/lib/components/Datepicker/datepickerTable.module.css +37 -0
- package/lib/components/Datepicker/index.ts +1 -0
- package/lib/components/Datepicker/useDatepicker.tsx +146 -0
- package/lib/components/Dropdown/DrawerOrDropdown.tsx +1 -1
- package/lib/components/Dropdown/dropdownBase.module.css +2 -1
- package/lib/components/Footer/FooterMenu.tsx +1 -1
- package/lib/components/GlobalMenu/AccountButton.tsx +20 -3
- package/lib/components/GlobalMenu/AccountMenu.tsx +13 -4
- package/lib/components/GlobalMenu/GlobalMenu.stories.tsx +6 -6
- package/lib/components/GlobalMenu/GlobalMenu.tsx +36 -24
- package/lib/components/Header/Header.stories.tsx +24 -8
- package/lib/components/Header/Header.tsx +15 -16
- package/lib/components/Header/HeaderButton.tsx +1 -0
- package/lib/components/Header/HeaderSearch.tsx +17 -0
- package/lib/components/Header/header.module.css +0 -21
- package/lib/components/Header/headerSearch.module.css +21 -0
- package/lib/components/History/HistoryList.stories.ts +3 -1
- package/lib/components/Layout/Layout.stories.tsx +7 -2
- package/lib/components/List/ListItemBase.tsx +4 -0
- package/lib/components/List/listBase.module.css +14 -0
- package/lib/components/List/listItemBase.module.css +9 -2
- package/lib/components/Menu/MenuInputField.tsx +38 -0
- package/lib/components/Menu/MenuItemBase.tsx +1 -1
- package/lib/components/Menu/MenuOption.tsx +1 -1
- package/lib/components/Menu/index.ts +1 -0
- package/lib/components/Menu/menuInputField.module.css +54 -0
- package/lib/components/Menu/menuItemBase.module.css +7 -0
- package/lib/components/Page/PageHeader.tsx +10 -6
- package/lib/components/Page/PageNav.tsx +34 -0
- package/lib/components/Page/pageHeader.module.css +13 -0
- package/lib/components/Page/pageNav.module.css +12 -0
- package/lib/components/Page/sectionBase.module.css +11 -1
- package/lib/components/RootProvider/RootProvider.tsx +1 -0
- package/lib/components/Searchbar/SearchField.tsx +4 -1
- package/lib/components/Searchbar/Searchbar.tsx +10 -2
- package/lib/components/Toolbar/Toolbar.tsx +4 -1
- package/lib/components/Toolbar/ToolbarAdd.tsx +3 -3
- package/lib/components/Toolbar/ToolbarDate.stories.ts +62 -0
- package/lib/components/Toolbar/ToolbarDaterange.stories.ts +24 -0
- package/lib/components/Toolbar/ToolbarDaterange.tsx +73 -0
- package/lib/components/Toolbar/ToolbarFilter.tsx +12 -5
- package/lib/components/Toolbar/ToolbarFilterBase.tsx +15 -0
- package/lib/components/Toolbar/ToolbarMenu.tsx +3 -3
- package/lib/components/Toolbar/ToolbarOptions.tsx +2 -2
- package/lib/components/Toolbar/toolbarDaterange.module.css +12 -0
- package/lib/components/Toolbar/{toolbarAdd.module.css → toolbarFilterBase.module.css} +2 -2
- package/lib/components/Typography/heading.module.css +1 -1
- package/lib/components/index.ts +3 -1
- package/lib/css/global.css +1 -0
- package/lib/hooks/useMenu.tsx +1 -0
- package/lib/stories/Inbox/BookmarksPage.tsx +38 -12
- package/lib/stories/Inbox/InboxLayout.tsx +1 -0
- package/lib/stories/Inbox/InboxPage.tsx +1 -0
- package/lib/stories/Inbox/InboxProvider.tsx +1 -1
- package/lib/stories/Inbox/InboxToolbar.tsx +3 -3
- package/lib/stories/Inbox/ProfilePage.tsx +1 -0
- package/lib/stories/Inbox/dialogs/brreg-draft.json +5 -9
- package/lib/stories/Inbox/dialogs/enova-in-progress.json +35 -0
- package/lib/stories/Inbox/dialogs/index.ts +44 -1
- package/lib/stories/Inbox/dialogs/sykmelding-referat.json +41 -0
- package/package.json +2 -2
- package/lib/components/Toolbar/toolbarFilter.module.css +0 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/Altinn/altinn-components/compare/v0.5.2...v0.6.0) (2024-11-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* date picker + bookmarks + saved searches ([#60](https://github.com/Altinn/altinn-components/issues/60)) ([3cc249e](https://github.com/Altinn/altinn-components/commit/3cc249ebbd6bb1f5157524a0bfa1fa6693f38dd8))
|
|
9
|
+
|
|
10
|
+
## [0.5.2](https://github.com/Altinn/altinn-components/compare/v0.5.1...v0.5.2) (2024-11-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* fixing selecting current account and simplify API for Header ([#58](https://github.com/Altinn/altinn-components/issues/58)) ([b39a14a](https://github.com/Altinn/altinn-components/commit/b39a14a14014ff78209d636f3edc93507b443fd2))
|
|
16
|
+
|
|
3
17
|
## [0.5.1](https://github.com/Altinn/altinn-components/compare/v0.5.0...v0.5.1) (2024-11-16)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ListBase, type ListSpacing } from '../';
|
|
2
|
+
import { BookmarksListItem, type BookmarksListItemProps } from './BookmarksListItem';
|
|
3
|
+
|
|
4
|
+
export interface BookmarksListProps {
|
|
5
|
+
items: BookmarksListItemProps[];
|
|
6
|
+
spacing: ListSpacing;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const BookmarksList = ({ items, spacing }: BookmarksListProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<ListBase spacing={spacing}>
|
|
12
|
+
{items?.map((item) => (
|
|
13
|
+
<BookmarksListItem {...item} key={item.id} />
|
|
14
|
+
))}
|
|
15
|
+
</ListBase>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Fragment, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { ListBase } from '..';
|
|
5
|
+
import { MetaItem } from '../Meta';
|
|
6
|
+
import { BookmarksListItem } from './BookmarksListItem';
|
|
7
|
+
|
|
8
|
+
const sizes = ['lg', 'md', 'sm', 'xs'];
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'Bookmarks/BookmarksListItem',
|
|
12
|
+
component: BookmarksListItem,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {},
|
|
15
|
+
args: {
|
|
16
|
+
id: 'id',
|
|
17
|
+
params: [
|
|
18
|
+
{
|
|
19
|
+
type: 'search',
|
|
20
|
+
label: 'mva',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'filter',
|
|
24
|
+
label: 'Skatteetaten',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
size: 'md',
|
|
28
|
+
href: '#',
|
|
29
|
+
},
|
|
30
|
+
} satisfies Meta<typeof BookmarksListItem>;
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof meta>;
|
|
34
|
+
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
args: {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const CustomTitle: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
title: 'Mitt lagrede søk',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const LotsOfParams: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
params: [
|
|
48
|
+
{
|
|
49
|
+
label: 'skatt',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'inkasso',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'filter',
|
|
56
|
+
label: 'Namsmannen',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'filter',
|
|
60
|
+
label: 'Denne uken',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const Sizes = (args) => {
|
|
67
|
+
return (
|
|
68
|
+
<ListBase>
|
|
69
|
+
{sizes?.map((size) => {
|
|
70
|
+
return (
|
|
71
|
+
<Fragment key={size}>
|
|
72
|
+
<BookmarksListItem {...args} size={size} selected={size === args?.size} />
|
|
73
|
+
<MetaItem>{size}</MetaItem>
|
|
74
|
+
</Fragment>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</ListBase>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ElementType } from 'react';
|
|
2
|
+
import { ListItemBase, ListItemLabel, ListItemMedia, type ListItemSize } from '..';
|
|
3
|
+
import { type QueryItemProps, QueryLabel } from './QueryLabel';
|
|
4
|
+
|
|
5
|
+
export interface BookmarksListItemProps {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Element type to render */
|
|
8
|
+
as?: ElementType;
|
|
9
|
+
/** ELement link */
|
|
10
|
+
href?: string;
|
|
11
|
+
/** Optional onClick */
|
|
12
|
+
onClick?(): void;
|
|
13
|
+
/** Size of list item */
|
|
14
|
+
size?: ListItemSize;
|
|
15
|
+
/** Optional title */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Optional description */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Query params */
|
|
20
|
+
params?: QueryItemProps[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const BookmarksListItem = ({ size = 'sm', title, description, params, ...rest }: BookmarksListItemProps) => {
|
|
24
|
+
return (
|
|
25
|
+
<ListItemBase size={size} linkIcon="chevron-right" {...rest}>
|
|
26
|
+
<ListItemMedia size={size} icon={'magnifying-glass'} />
|
|
27
|
+
<ListItemLabel title={title} size={size}>
|
|
28
|
+
{!title && <QueryLabel params={params} />}
|
|
29
|
+
</ListItemLabel>
|
|
30
|
+
</ListItemBase>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import styles from './queryLabel.module.css';
|
|
2
|
+
export type QueryItemType = 'search' | 'filter';
|
|
3
|
+
|
|
4
|
+
export interface QueryItemProps {
|
|
5
|
+
type?: QueryItemType;
|
|
6
|
+
value?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const QueryItem = ({ type = 'search', label }: QueryItemProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<mark className={styles.item} data-type={type}>
|
|
13
|
+
{label}
|
|
14
|
+
</mark>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface QueryLabelProps {
|
|
19
|
+
params?: QueryItemProps[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const QueryLabel = ({ params = [] }: QueryLabelProps) => {
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.label}>
|
|
25
|
+
{params.map((item, index) => (
|
|
26
|
+
<div className={styles.group} key={index}>
|
|
27
|
+
<QueryItem {...item} />
|
|
28
|
+
{params[index + 1] && <span className={styles.plus}>+</span>}
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.label {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
align-items: center;
|
|
5
|
+
column-gap: 0.25rem;
|
|
6
|
+
row-gap: 0.25rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.group {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
column-gap: 0.25rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.plus {
|
|
16
|
+
color: var(--theme-text-subtle);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.item {
|
|
20
|
+
font-size: 0.875em;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.item[data-type="search"] {
|
|
24
|
+
background-color: transparent;
|
|
25
|
+
font-weight: 500;
|
|
26
|
+
text-decoration: underline;
|
|
27
|
+
text-decoration-thickness: 1px;
|
|
28
|
+
text-underline-offset: 0.25em;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.item[data-type="search"]:before {
|
|
32
|
+
content: "«";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.item[data-type="search"]:after {
|
|
36
|
+
content: "»";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.item[data-type="filter"] {
|
|
40
|
+
background-color: transparent;
|
|
41
|
+
border: 1px solid;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
padding: 0.25em 0.5em;
|
|
44
|
+
border-radius: 2px;
|
|
45
|
+
}
|
|
@@ -32,12 +32,13 @@ export const ButtonBase = ({
|
|
|
32
32
|
size,
|
|
33
33
|
selected,
|
|
34
34
|
variant,
|
|
35
|
+
tabIndex = 0,
|
|
35
36
|
...rest
|
|
36
37
|
}: ButtonBaseProps) => {
|
|
37
38
|
const Component = as || 'button';
|
|
38
39
|
return (
|
|
39
40
|
<Component
|
|
40
|
-
tabIndex={
|
|
41
|
+
tabIndex={tabIndex}
|
|
41
42
|
data-size={size}
|
|
42
43
|
data-variant={variant}
|
|
43
44
|
data-color={color}
|
|
@@ -10,6 +10,7 @@ interface IconButtonProps {
|
|
|
10
10
|
icon: IconName;
|
|
11
11
|
color?: ButtonColor;
|
|
12
12
|
size?: ButtonSize;
|
|
13
|
+
iconSize?: ButtonSize;
|
|
13
14
|
variant?: ButtonVariant;
|
|
14
15
|
className?: string;
|
|
15
16
|
onClick?: MouseEventHandler;
|
|
@@ -20,12 +21,13 @@ export const IconButton = ({
|
|
|
20
21
|
color = 'primary',
|
|
21
22
|
size = 'md',
|
|
22
23
|
icon,
|
|
24
|
+
iconSize,
|
|
23
25
|
className,
|
|
24
26
|
onClick,
|
|
25
27
|
}: IconButtonProps) => {
|
|
26
28
|
return (
|
|
27
29
|
<ButtonBase variant={variant} color={color} size={size} className={cx(styles.button, className)} onClick={onClick}>
|
|
28
|
-
<ButtonIcon icon={icon} size={size} />
|
|
30
|
+
<ButtonIcon icon={icon} size={iconSize || size} />
|
|
29
31
|
</ButtonBase>
|
|
30
32
|
);
|
|
31
33
|
};
|
|
@@ -17,10 +17,12 @@ export const ContextMenuBase = ({
|
|
|
17
17
|
children,
|
|
18
18
|
}: ContextMenuBaseProps) => {
|
|
19
19
|
return (
|
|
20
|
-
<div className={styles.toggle}>
|
|
20
|
+
<div className={styles.toggle} data-theme="neutral">
|
|
21
21
|
<IconButton
|
|
22
22
|
className={styles.button}
|
|
23
|
+
size="sm"
|
|
23
24
|
icon="menu-elipsis-horizontal"
|
|
25
|
+
iconSize="md"
|
|
24
26
|
variant="text"
|
|
25
27
|
color="secondary"
|
|
26
28
|
onClick={onToggle}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { DatepickerBase } from './DatepickerBase';
|
|
4
|
+
import { DatepickerHeader } from './DatepickerHeader';
|
|
5
|
+
import { DatepickerTable } from './DatepickerTable';
|
|
6
|
+
import { useDatepicker } from './useDatepicker';
|
|
7
|
+
|
|
8
|
+
export interface DatepickerProps {
|
|
9
|
+
defaultDate?: string;
|
|
10
|
+
months?: string[];
|
|
11
|
+
weekdays?: string[];
|
|
12
|
+
selectFrom?: string;
|
|
13
|
+
selectTo?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
onSelect: (isoDate: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export const Datepicker = ({
|
|
18
|
+
months = [
|
|
19
|
+
'January',
|
|
20
|
+
'February',
|
|
21
|
+
'March',
|
|
22
|
+
'April',
|
|
23
|
+
'May',
|
|
24
|
+
'June',
|
|
25
|
+
'July',
|
|
26
|
+
'August',
|
|
27
|
+
'September',
|
|
28
|
+
'October',
|
|
29
|
+
'November',
|
|
30
|
+
'December',
|
|
31
|
+
],
|
|
32
|
+
weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
|
33
|
+
defaultDate,
|
|
34
|
+
selectFrom,
|
|
35
|
+
selectTo,
|
|
36
|
+
className,
|
|
37
|
+
onSelect,
|
|
38
|
+
}: DatepickerProps) => {
|
|
39
|
+
const [date, setDate] = useState(defaultDate);
|
|
40
|
+
|
|
41
|
+
const { month, rows, next, prev } = useDatepicker({ date, selectFrom, selectTo });
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<DatepickerBase className={className}>
|
|
45
|
+
<DatepickerHeader title={months[month]} onNext={() => setDate(next)} onPrev={() => setDate(prev)} />
|
|
46
|
+
<DatepickerTable rows={rows} weekdays={weekdays} onSelect={onSelect} />
|
|
47
|
+
</DatepickerBase>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import styles from './datepickerBase.module.css';
|
|
4
|
+
|
|
5
|
+
export interface DatepickerBaseProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DatepickerBase = ({ className, children }: DatepickerBaseProps) => {
|
|
11
|
+
return <div className={cx(styles.datepicker, className)}>{children}</div>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IconButton } from '../Button';
|
|
2
|
+
import styles from './datepickerHeader.module.css';
|
|
3
|
+
|
|
4
|
+
export interface DatepickerHeaderProps {
|
|
5
|
+
title?: string;
|
|
6
|
+
onPrev: () => void;
|
|
7
|
+
onNext: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DatepickerHeader = ({ title, onNext, onPrev }: DatepickerHeaderProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<header className={styles.header}>
|
|
13
|
+
<h2 className={styles.title}>{title}</h2>
|
|
14
|
+
<nav className={styles.nav}>
|
|
15
|
+
<IconButton icon="chevron-left" size="sm" variant="text" onClick={onPrev} />
|
|
16
|
+
<IconButton icon="chevron-right" size="sm" variant="text" onClick={onNext} />
|
|
17
|
+
</nav>
|
|
18
|
+
</header>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ButtonBase } from '../Button';
|
|
2
|
+
import styles from './datepickerTable.module.css';
|
|
3
|
+
import type { DatepickerDate } from './useDatepicker';
|
|
4
|
+
|
|
5
|
+
export interface DatepickerTableProps {
|
|
6
|
+
weekdays: string[];
|
|
7
|
+
onSelect: (date: string) => void;
|
|
8
|
+
rows?: DatepickerDate[][];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DatepickerTable = ({
|
|
12
|
+
weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
|
13
|
+
rows = [],
|
|
14
|
+
onSelect,
|
|
15
|
+
}: DatepickerTableProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<table className={styles.table}>
|
|
18
|
+
<thead>
|
|
19
|
+
<tr>
|
|
20
|
+
{weekdays.map((weekday) => (
|
|
21
|
+
<th key={weekday} className={styles.weekday}>
|
|
22
|
+
{weekday.substring(0, 2)}
|
|
23
|
+
</th>
|
|
24
|
+
))}
|
|
25
|
+
</tr>
|
|
26
|
+
</thead>
|
|
27
|
+
<tbody>
|
|
28
|
+
{rows.map((row, i) => (
|
|
29
|
+
<tr key={i}>
|
|
30
|
+
{row.map((item) => {
|
|
31
|
+
const { day, date, selected, isCurrentMonth } = item;
|
|
32
|
+
return (
|
|
33
|
+
<td key={date} className={styles.date} data-current-month={isCurrentMonth}>
|
|
34
|
+
<ButtonBase
|
|
35
|
+
variant="text"
|
|
36
|
+
selected={selected}
|
|
37
|
+
className={styles.button}
|
|
38
|
+
onClick={() => onSelect(date)}
|
|
39
|
+
>
|
|
40
|
+
{day}
|
|
41
|
+
</ButtonBase>
|
|
42
|
+
</td>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</tr>
|
|
46
|
+
))}
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.table {
|
|
2
|
+
border-collapse: collapse;
|
|
3
|
+
table-layout: fixed;
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.weekday {
|
|
8
|
+
font-size: 0.75rem;
|
|
9
|
+
text-align: center;
|
|
10
|
+
padding: 0.25rem 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.date {
|
|
14
|
+
background-color: transparent;
|
|
15
|
+
color: var(--theme-text-subtle);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.date[data-current-month="true"] {
|
|
19
|
+
background-color: var(--theme-background-subtle);
|
|
20
|
+
color: var(--theme-text-default);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.button {
|
|
24
|
+
font-size: 0.875rem;
|
|
25
|
+
width: 100%;
|
|
26
|
+
text-align: center;
|
|
27
|
+
height: 2.25rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.button[aria-selected="true"] {
|
|
31
|
+
background-color: transparent;
|
|
32
|
+
border: 1px solid var(--theme-border-default);
|
|
33
|
+
}
|
|
34
|
+
.button[aria-selected="true"]:hover,
|
|
35
|
+
.button:hover {
|
|
36
|
+
background-color: var(--theme-surface-hover);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Datepicker';
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface DatepickerDate {
|
|
5
|
+
/** Day of the month */
|
|
6
|
+
day: number;
|
|
7
|
+
/** ISO date string in YYYY-MM-DD format */
|
|
8
|
+
date: string;
|
|
9
|
+
/** Indicates if the day is selected */
|
|
10
|
+
selected: boolean;
|
|
11
|
+
/** Indicates if the day belongs to the current month */
|
|
12
|
+
isCurrentMonth: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents a day object in the datepicker.
|
|
17
|
+
*/
|
|
18
|
+
interface DayObject extends DatepickerDate {
|
|
19
|
+
/** Year of the date */
|
|
20
|
+
year: number;
|
|
21
|
+
/** Zero-based month (0 = January, 11 = December) */
|
|
22
|
+
month: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface UseDatepickerOutput {
|
|
26
|
+
/** Year of the calendar */
|
|
27
|
+
year: number;
|
|
28
|
+
/** Zero-based month of the calendar (0 = January, 11 = December) */
|
|
29
|
+
month: number;
|
|
30
|
+
/** Weeks in the calendar */
|
|
31
|
+
rows: DayObject[][];
|
|
32
|
+
/** Day of the month */
|
|
33
|
+
day: number;
|
|
34
|
+
/** ISO date string for the next month */
|
|
35
|
+
next: string;
|
|
36
|
+
/** ISO date string for the previous month */
|
|
37
|
+
prev: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Props for the useDatepicker hook.
|
|
42
|
+
*/
|
|
43
|
+
interface UseDatepickerProps {
|
|
44
|
+
/** ISO date string for the calendar month */
|
|
45
|
+
date?: string;
|
|
46
|
+
/** Optional ISO date string for the start of the selection range */
|
|
47
|
+
selectFrom?: string;
|
|
48
|
+
/** Optional ISO date string for the end of the selection range */
|
|
49
|
+
selectTo?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const useDatepicker = ({ date, selectFrom, selectTo }: UseDatepickerProps): UseDatepickerOutput => {
|
|
53
|
+
return useMemo(() => {
|
|
54
|
+
const generateIsoDate = (year: number, month: number, day: number) => {
|
|
55
|
+
const paddedMonth = String(month + 1).padStart(2, '0');
|
|
56
|
+
const paddedDay = String(day).padStart(2, '0');
|
|
57
|
+
return `${year}-${paddedMonth}-${paddedDay}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const isSelected = (isoDate: string) => {
|
|
61
|
+
if (!fromTimestamp && !toTimestamp) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dateTimestamp = new Date(isoDate).getTime();
|
|
66
|
+
return (
|
|
67
|
+
(fromTimestamp === null || dateTimestamp >= fromTimestamp) &&
|
|
68
|
+
(toTimestamp === null || dateTimestamp <= toTimestamp)
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const baseDate = date ? new Date(date) : new Date();
|
|
73
|
+
const year = baseDate.getFullYear();
|
|
74
|
+
const month = baseDate.getMonth();
|
|
75
|
+
|
|
76
|
+
const fromTimestamp = selectFrom ? new Date(selectFrom).getTime() : null;
|
|
77
|
+
const toTimestamp = selectTo ? new Date(selectTo).getTime() : null;
|
|
78
|
+
|
|
79
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
80
|
+
const firstDayOfMonth = (new Date(year, month, 1).getDay() + 6) % 7; // Start weeks on Monday
|
|
81
|
+
const daysInPrevMonth = new Date(year, month, 0).getDate();
|
|
82
|
+
|
|
83
|
+
const rows: DayObject[][] = [];
|
|
84
|
+
let currentDay = 1;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < 6; i++) {
|
|
87
|
+
const week: DayObject[] = [];
|
|
88
|
+
for (let j = 0; j < 7; j++) {
|
|
89
|
+
if (i === 0 && j < firstDayOfMonth) {
|
|
90
|
+
// Days from the previous month
|
|
91
|
+
const day = daysInPrevMonth - firstDayOfMonth + j + 1;
|
|
92
|
+
const prevMonth = month === 0 ? 11 : month - 1;
|
|
93
|
+
const prevYear = month === 0 ? year - 1 : year;
|
|
94
|
+
const isoDate = generateIsoDate(prevYear, prevMonth, day);
|
|
95
|
+
week.push({
|
|
96
|
+
year: prevYear,
|
|
97
|
+
month: prevMonth,
|
|
98
|
+
day,
|
|
99
|
+
isCurrentMonth: false,
|
|
100
|
+
date: isoDate,
|
|
101
|
+
selected: isSelected(isoDate),
|
|
102
|
+
});
|
|
103
|
+
} else if (currentDay > daysInMonth) {
|
|
104
|
+
// Days from the next month
|
|
105
|
+
const day = currentDay - daysInMonth;
|
|
106
|
+
const nextMonth = month === 11 ? 0 : month + 1;
|
|
107
|
+
const nextYear = month === 11 ? year + 1 : year;
|
|
108
|
+
const isoDate = generateIsoDate(nextYear, nextMonth, day);
|
|
109
|
+
week.push({
|
|
110
|
+
year: nextYear,
|
|
111
|
+
month: nextMonth,
|
|
112
|
+
day,
|
|
113
|
+
isCurrentMonth: false,
|
|
114
|
+
date: isoDate,
|
|
115
|
+
selected: isSelected(isoDate),
|
|
116
|
+
});
|
|
117
|
+
currentDay++;
|
|
118
|
+
} else {
|
|
119
|
+
// Days from the current month
|
|
120
|
+
const isoDate = generateIsoDate(year, month, currentDay);
|
|
121
|
+
week.push({
|
|
122
|
+
year,
|
|
123
|
+
month,
|
|
124
|
+
day: currentDay,
|
|
125
|
+
isCurrentMonth: true,
|
|
126
|
+
date: isoDate,
|
|
127
|
+
selected: isSelected(isoDate),
|
|
128
|
+
});
|
|
129
|
+
currentDay++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
rows.push(week);
|
|
133
|
+
if (currentDay > daysInMonth) break; // Stop once we've covered all days
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const prevMonth = month === 0 ? 11 : month - 1;
|
|
137
|
+
const prevYear = month === 0 ? year - 1 : year;
|
|
138
|
+
const prev = generateIsoDate(prevYear, prevMonth, 1);
|
|
139
|
+
|
|
140
|
+
const nextMonth = month === 11 ? 0 : month + 1;
|
|
141
|
+
const nextYear = month === 11 ? year + 1 : year;
|
|
142
|
+
const next = generateIsoDate(nextYear, nextMonth, 1);
|
|
143
|
+
|
|
144
|
+
return { year, month, rows, day: currentDay, next, prev };
|
|
145
|
+
}, [date, selectFrom, selectTo]);
|
|
146
|
+
};
|
|
@@ -16,7 +16,7 @@ export const DrawerOrDropdown = ({ expanded = false, title, onClose, button, chi
|
|
|
16
16
|
return (
|
|
17
17
|
<>
|
|
18
18
|
{expanded && <Backdrop onClick={onClose} />}
|
|
19
|
-
<DropdownBase className={styles.dropdown} expanded={expanded}>
|
|
19
|
+
<DropdownBase className={styles.dropdown} padding={true} expanded={expanded}>
|
|
20
20
|
{children}
|
|
21
21
|
</DropdownBase>
|
|
22
22
|
<DrawerBase className={styles.drawer} placement="bottom" expanded={expanded}>
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
right: 0;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
.dropdown[data-padding="
|
|
19
|
+
.dropdown[data-padding="true"] {
|
|
20
20
|
padding: 0 0.5rem;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
.dropdown {
|
|
24
24
|
min-width: 14rem;
|
|
25
|
+
max-width: 20rem;
|
|
25
26
|
margin-top: 0.5rem;
|
|
26
27
|
background-color: var(--neutral-background-default);
|
|
27
28
|
border-radius: 0.375rem;
|