@tpzdsp/next-toolkit 3.0.0 → 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpzdsp/next-toolkit",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "A reusable React component library for Next.js applications",
5
5
  "packageManager": "pnpm@11.5.3",
6
6
  "engines": {
@@ -17,9 +17,11 @@
17
17
  /* min-w-0 lets this column shrink below its content's natural width
18
18
  within the flex row, same reasoning as elsewhere in this project:
19
19
  without it, long unbroken content (e.g. a digest UUID) could force the
20
- row wider than the modal itself instead of wrapping/scrolling. */
20
+ row wider than the modal itself instead of wrapping/scrolling. flex and
21
+ flex-col themselves come from Stack (this is applied to one via
22
+ ErrorModal.tsx's className), not repeated here. */
21
23
  .cn-error-modal-body {
22
- @apply flex min-w-0 flex-col;
24
+ @apply min-w-0;
23
25
  }
24
26
 
25
27
  /* Scoped to the summary list only (not the copy button/hint below it), an
@@ -5,7 +5,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
5
5
  import { ErrorModal } from './ErrorModal';
6
6
  import { ApiError } from '../../errors/ApiError';
7
7
  import { Button } from '../Button/Button';
8
- import { Paragraph } from '../Paragraph/Paragraph';
9
8
 
10
9
  const meta: Meta<typeof ErrorModal> = {
11
10
  title: 'Components/ErrorModal',
@@ -41,7 +40,7 @@ export const Default: Story = {
41
40
  onOpenChange={setOpen}
42
41
  error={new Error('Failed to fetch sampling points')}
43
42
  >
44
- <Paragraph>An error occurred while fetching sampling points.</Paragraph>
43
+ An error occurred while fetching sampling points.
45
44
  </ErrorModal>
46
45
  </>
47
46
  );
@@ -69,7 +68,7 @@ export const WithApiError: Story = {
69
68
  onOpenChange={setOpen}
70
69
  error={ApiError.internalServerError('The upstream sampling points service timed out')}
71
70
  >
72
- <Paragraph>An error occurred while fetching sampling points.</Paragraph>
71
+ An error occurred while fetching sampling points.
73
72
  </ErrorModal>
74
73
  </>
75
74
  );
@@ -93,7 +92,7 @@ export const WithoutError: Story = {
93
92
  <Button onClick={() => setOpen(true)}>Trigger error</Button>
94
93
 
95
94
  <ErrorModal open={open} onOpenChange={setOpen}>
96
- <Paragraph>Something went wrong. Try again in a few minutes.</Paragraph>
95
+ Something went wrong. Try again in a few minutes.
97
96
  </ErrorModal>
98
97
  </>
99
98
  );
@@ -52,68 +52,71 @@ export const ErrorModal = ({
52
52
  <LuCircleAlert aria-hidden className="size-8" />
53
53
  </div>
54
54
 
55
- <div className="cn-error-modal-body">
55
+ <Stack gap="sm" className="cn-error-modal-body">
56
56
  <ModalHeader>
57
57
  <ModalTitle>{title}</ModalTitle>
58
58
  </ModalHeader>
59
59
 
60
- <ModalDescription>
61
- <Stack gap="sm">
62
- {children}
63
-
64
- {info ? (
65
- <Details>
66
- <DetailsTrigger>Technical information</DetailsTrigger>
67
-
68
- <DetailsContent>
69
- <Stack gap="sm">
70
- <div className="cn-error-modal-details">
71
- <SummaryList>
72
- <SummaryListRow>
73
- <SummaryListKey>Reason</SummaryListKey>
74
-
75
- <SummaryListValue>{info.message}</SummaryListValue>
76
- </SummaryListRow>
77
-
78
- {info.details ? (
79
- <SummaryListRow>
80
- <SummaryListKey>Details</SummaryListKey>
81
-
82
- <SummaryListValue>{info.details}</SummaryListValue>
83
- </SummaryListRow>
84
- ) : null}
85
-
86
- {info.digest ? (
87
- <SummaryListRow>
88
- <SummaryListKey>Digest</SummaryListKey>
89
-
90
- <SummaryListValue>{info.digest}</SummaryListValue>
91
- </SummaryListRow>
92
- ) : null}
93
- </SummaryList>
94
- </div>
95
-
96
- <div className="flex flex-col gap-gds-1">
97
- <CopyButton
98
- appearance="secondary"
99
- content={formatErrorForClipboard(info)}
100
- />
101
-
102
- <HintText className="text-sm">
103
- Copy to share error details with the app administrator.
104
- </HintText>
105
- </div>
106
- </Stack>
107
- </DetailsContent>
108
- </Details>
109
- ) : null}
110
- </Stack>
111
- </ModalDescription>
60
+ {/* ModalDescription renders a <p> (it's the target of the
61
+ dialog's aria-describedby), which can only contain phrasing
62
+ content. children is assumed to be plain descriptive text for
63
+ that reason, Details below is a sibling instead, not nested
64
+ inside it, both to stay valid HTML (a browser silently closes
65
+ a <p> early upon hitting a nested block element, which desyncs
66
+ SSR/client markup into a hydration error, exactly like nesting
67
+ a <dl> in one here did) and because interactive elements like
68
+ the copy button below don't make sense as part of an
69
+ aria-describedby'd description in the first place. */}
70
+ <ModalDescription>{children}</ModalDescription>
71
+
72
+ {info ? (
73
+ <Details>
74
+ <DetailsTrigger>Technical information</DetailsTrigger>
75
+
76
+ <DetailsContent>
77
+ <Stack gap="sm">
78
+ <div className="cn-error-modal-details">
79
+ <SummaryList>
80
+ <SummaryListRow>
81
+ <SummaryListKey>Reason</SummaryListKey>
82
+
83
+ <SummaryListValue>{info.message}</SummaryListValue>
84
+ </SummaryListRow>
85
+
86
+ {info.details ? (
87
+ <SummaryListRow>
88
+ <SummaryListKey>Details</SummaryListKey>
89
+
90
+ <SummaryListValue>{info.details}</SummaryListValue>
91
+ </SummaryListRow>
92
+ ) : null}
93
+
94
+ {info.digest ? (
95
+ <SummaryListRow>
96
+ <SummaryListKey>Digest</SummaryListKey>
97
+
98
+ <SummaryListValue>{info.digest}</SummaryListValue>
99
+ </SummaryListRow>
100
+ ) : null}
101
+ </SummaryList>
102
+ </div>
103
+
104
+ <div className="flex flex-col gap-gds-1">
105
+ <CopyButton appearance="secondary" content={formatErrorForClipboard(info)} />
106
+
107
+ <HintText className="text-sm">
108
+ Copy to share error details with the app administrator.
109
+ </HintText>
110
+ </div>
111
+ </Stack>
112
+ </DetailsContent>
113
+ </Details>
114
+ ) : null}
112
115
 
113
116
  <ModalFooter className="sm:justify-start">
114
117
  <Button onClick={() => onOpenChange?.(false)}>{closeLabel}</Button>
115
118
  </ModalFooter>
116
- </div>
119
+ </Stack>
117
120
  </ModalContent>
118
121
  </Modal>
119
122
  );
@@ -7,7 +7,6 @@ export const Copyright = () => {
7
7
  <ExternalLink
8
8
  className="text-balance sm:text-nowrap text-center sm:text-left justify-start"
9
9
  appearance="contrast"
10
- variant="plain"
11
10
  href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/"
12
11
  >
13
12
  &copy; Crown Copyright {currentYear}
@@ -15,6 +15,9 @@ const meta = {
15
15
  },
16
16
  },
17
17
  tags: ['autodocs'],
18
+ args: {
19
+ accessibilityStatementHref: '/accessibility-statement',
20
+ },
18
21
  } satisfies Meta<typeof Footer>;
19
22
 
20
23
  export default meta;
@@ -4,7 +4,12 @@ import { MetaLinks } from './MetaLinks';
4
4
  import { DefraLogo } from '../../assets/images/DefraLogo';
5
5
  import { WidthContainer } from '../Layout/WidthContainer/WidthContainer';
6
6
 
7
- export const Footer = () => (
7
+ export type FooterProps = {
8
+ /** Passed straight through to MetaLinks, see its own accessibilityStatementHref doc. */
9
+ accessibilityStatementHref?: string;
10
+ };
11
+
12
+ export const Footer = ({ accessibilityStatementHref }: FooterProps) => (
8
13
  <footer className="bg-wash">
9
14
  <WidthContainer className="py-gds-3">
10
15
  <div
@@ -15,7 +20,7 @@ export const Footer = () => (
15
20
  <nav className="row-start-1 col-start-1 w-full pb-gds-2 self-end">
16
21
  <h2 className="sr-only">Support Links</h2>
17
22
 
18
- <MetaLinks />
23
+ <MetaLinks accessibilityStatementHref={accessibilityStatementHref} />
19
24
  </nav>
20
25
 
21
26
  <div className="row-start-2 col-start-1 self-end pt-gds-1">
@@ -1,10 +1,6 @@
1
- import { ExternalLink } from '../ExternalLink/ExternalLink';
1
+ import { Link } from '../Link/Link';
2
2
 
3
- const LINKS = [
4
- {
5
- label: 'Accessibility statement',
6
- url: '/support/faqs/275810340/275810354',
7
- },
3
+ const STATIC_LINKS = [
8
4
  {
9
5
  label: 'Cookie Policy',
10
6
  url: '/help/cookies',
@@ -23,19 +19,29 @@ const LINKS = [
23
19
  },
24
20
  ];
25
21
 
26
- export const MetaLinks = () => (
27
- <ul className="flex w-full flex-wrap gap-x-gds-4 gap-y-gds-2 md:gap-gds-5 md:justify-start">
28
- {LINKS.map((link) => (
29
- <li className="flex" key={link.label}>
30
- <ExternalLink
31
- className="md:text-left"
32
- appearance="contrast"
33
- href={link.url}
34
- variant="plain"
35
- >
36
- {link.label}
37
- </ExternalLink>
38
- </li>
39
- ))}
40
- </ul>
41
- );
22
+ export type MetaLinksProps = {
23
+ /** No single accessibility statement applies across every app this footer
24
+ * ships in, so unlike the other, shared support links above, this one has
25
+ * to be supplied by the consuming app rather than hardcoded here. Optional
26
+ * for now, apps that don't have one yet just don't get the link, rather
27
+ * than one pointing nowhere. */
28
+ accessibilityStatementHref?: string;
29
+ };
30
+
31
+ export const MetaLinks = ({ accessibilityStatementHref }: MetaLinksProps) => {
32
+ const links = accessibilityStatementHref
33
+ ? [{ label: 'Accessibility statement', url: accessibilityStatementHref }, ...STATIC_LINKS]
34
+ : STATIC_LINKS;
35
+
36
+ return (
37
+ <ul className="flex w-full flex-wrap gap-x-gds-4 gap-y-gds-2 md:gap-gds-5 md:justify-start">
38
+ {links.map((link) => (
39
+ <li className="flex" key={link.label}>
40
+ <Link className="md:text-left" appearance="contrast" href={link.url}>
41
+ {link.label}
42
+ </Link>
43
+ </li>
44
+ ))}
45
+ </ul>
46
+ );
47
+ };
@@ -15,6 +15,9 @@ const meta = {
15
15
  },
16
16
  },
17
17
  tags: ['autodocs'],
18
+ args: {
19
+ accessibilityStatementHref: '/accessibility-statement',
20
+ },
18
21
  } satisfies Meta<typeof SmallFooter>;
19
22
 
20
23
  export default meta;
@@ -3,6 +3,7 @@ import { EaLogo } from '../../../assets/images/EaLogo';
3
3
  import { OglLogo } from '../../../assets/images/OglLogo';
4
4
  import { ExternalLink } from '../../ExternalLink/ExternalLink';
5
5
  import { WidthContainer } from '../../Layout/WidthContainer/WidthContainer';
6
+ import { Link } from '../../Link/Link';
6
7
 
7
8
  const CROWN_COPYRIGHT_URL =
8
9
  'https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/';
@@ -13,11 +14,15 @@ const LICENSING_DETAILS_URL =
13
14
  export type SmallFooterProps = {
14
15
  eaLogoPrimaryFill?: string;
15
16
  eaLogoHighlightFill?: string;
17
+ /** Same as Footer's own accessibilityStatementHref: no universal default,
18
+ * omit it to hide the link entirely rather than pointing at nothing. */
19
+ accessibilityStatementHref?: string;
16
20
  };
17
21
 
18
22
  export const SmallFooter = ({
19
23
  eaLogoPrimaryFill = '#F3F2F1',
20
24
  eaLogoHighlightFill = '#79A12F',
25
+ accessibilityStatementHref,
21
26
  }: SmallFooterProps) => (
22
27
  <footer aria-label="Site footer" className="flex justify-center">
23
28
  <WidthContainer className="bg-wash py-gds-2">
@@ -32,6 +37,12 @@ export const SmallFooter = ({
32
37
  </div>
33
38
 
34
39
  <div className="flex items-center gap-gds-6 xs:gap-gds-2">
40
+ {accessibilityStatementHref ? (
41
+ <Link appearance="contrast" href={accessibilityStatementHref}>
42
+ Accessibility statement
43
+ </Link>
44
+ ) : null}
45
+
35
46
  <ExternalLink
36
47
  appearance="contrast"
37
48
  variant="plain"
@@ -17,7 +17,6 @@ import {
17
17
  Separator,
18
18
  WidthContainer,
19
19
  Popover,
20
- Paragraph,
21
20
  ParagraphGroup,
22
21
  } from '@tpzdsp/next-toolkit/components';
23
22
  import { cn } from '@tpzdsp/next-toolkit/utils';
@@ -144,7 +143,7 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
144
143
 
145
144
  <abbr
146
145
  title="Department for Environment, Food, and Rural Affairs"
147
- className="sr-only xs:not-sr-only"
146
+ className="sr-only xs:not-sr-only no-underline"
148
147
  >
149
148
  DEFRA
150
149
  </abbr>
@@ -156,7 +155,9 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
156
155
  <span className="hidden xs:inline">Data Services Platform</span>
157
156
 
158
157
  <span className="inline xs:hidden">
159
- <abbr title="Data Services Platform">DSP</abbr>
158
+ <abbr title="Data Services Platform" className="no-underline">
159
+ DSP
160
+ </abbr>
160
161
  </span>
161
162
  </Link>
162
163
  </div>
@@ -165,9 +166,9 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
165
166
  <ul className="flex items-center gap-gds-4">
166
167
  {DSP_NAV_LINKS.map((item) => (
167
168
  <li className={cn('flex items-center nav-item', item.mainNav)} key={item.href}>
168
- <ExternalLink href={item.href} appearance="inverse" variant="plain">
169
+ <Link href={item.href} appearance="inverse">
169
170
  {item.label}
170
- </ExternalLink>
171
+ </Link>
171
172
  </li>
172
173
  ))}
173
174
  <li ref={triggerRef} className="flex items-center more-btn shrink-0">
@@ -195,11 +196,11 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
195
196
 
196
197
  <PopoverContent sideOffset={0}>
197
198
  <ParagraphGroup>
198
- <Paragraph>
199
+ <div>
199
200
  <div className="font-bold">Signed in as:</div>
200
201
 
201
202
  <div>{credentials.user.email}</div>
202
- </Paragraph>
203
+ </div>
203
204
 
204
205
  <Link href={`${LOGOUT_URL}?redirect_uri=${redirectUri}`}>Sign-out</Link>
205
206
  </ParagraphGroup>
@@ -273,9 +274,9 @@ export const Header = ({ appContent, credentials }: HeaderProps) => {
273
274
  <ul className="flex flex-wrap w-full gap-x-gds-2 gap-y-gds-2">
274
275
  {DSP_NAV_LINKS.map((item) => (
275
276
  <li className={cn('flex items-center nav-item', item.drawerNav)} key={item.href}>
276
- <ExternalLink href={item.href} appearance="inverse" variant="plain">
277
+ <Link href={item.href} appearance="inverse">
277
278
  {item.label}
278
- </ExternalLink>
279
+ </Link>
279
280
  </li>
280
281
  ))}
281
282
  </ul>
@@ -80,7 +80,7 @@ export const Static: Story = {
80
80
  <LongContent />
81
81
  </MainWrapper>
82
82
 
83
- <Footer />
83
+ <Footer accessibilityStatementHref="/accessibility-statement" />
84
84
  </PageShell>
85
85
  ),
86
86
  };
@@ -102,7 +102,7 @@ export const ScrollLocked: Story = {
102
102
  <LongContent />
103
103
  </MainWrapper>
104
104
 
105
- <Footer />
105
+ <Footer accessibilityStatementHref="/accessibility-statement" />
106
106
  </PageShell>
107
107
  ),
108
108
  };
@@ -91,16 +91,13 @@
91
91
  }
92
92
 
93
93
  /* MARK: Vertical rhythm between sections
94
- Adjacency-based rather than flex+gap on cn-modal-content itself,
95
- ModalContent needs to stay a plain block so consumers can turn it into a
96
- flex row instead (e.g. ErrorModal, an icon beside the content) without
97
- fighting a hardcoded flex-direction. Applies wherever these three end up
98
- as direct siblings, regardless of what wraps them, ModalDescription is
99
- usually a sibling of ModalHeader rather than nested inside it. */
100
- :where(.cn-modal-header, .cn-modal-description, .cn-modal-footer)
101
- + :where(.cn-modal-header, .cn-modal-description, .cn-modal-footer) {
102
- margin-top: var(--spacing-gds-4);
103
- }
94
+ No layout opinion of its own on cn-modal-content, it needs to stay a
95
+ plain block so consumers can turn it into a flex row instead (e.g.
96
+ ErrorModal, an icon beside the content) without fighting a hardcoded
97
+ flex-direction. Wrap whatever sits inside (ModalHeader, ModalDescription,
98
+ ModalFooter, and anything else a specific modal needs, a form, a
99
+ Details section, ...) in a Stack instead, same as any other arbitrary
100
+ vertical content elsewhere in the library. */
104
101
 
105
102
  /* MARK: Modal title and description */
106
103
 
@@ -13,6 +13,11 @@ import {
13
13
  ModalTrigger,
14
14
  } from './Modal';
15
15
  import { Button } from '../Button/Button';
16
+ import { FormGroup } from '../Form/FormGroup';
17
+ import { HintText } from '../HintText/HintText';
18
+ import { Input } from '../Input/Input';
19
+ import { Label } from '../Label/Label';
20
+ import { Stack } from '../Layout/Stack/Stack';
16
21
 
17
22
  const meta: Meta<typeof Modal> = {
18
23
  title: 'Components/Modal',
@@ -21,7 +26,8 @@ const meta: Meta<typeof Modal> = {
21
26
  parameters: {
22
27
  docs: {
23
28
  description: {
24
- component: 'Use the Modal primitives to display dialog content in an overlay.',
29
+ component:
30
+ "Use the Modal primitives to display dialog content in an overlay. ModalContent has no layout opinion of its own (so consumers can turn it into something other than a column, e.g. ErrorModal's icon-beside-content row), wrap ModalHeader/ModalDescription/ModalFooter and anything else in a Stack for spacing between them, as below.",
25
31
  },
26
32
  },
27
33
  },
@@ -45,23 +51,75 @@ export const Default: Story = {
45
51
  </ModalTrigger>
46
52
 
47
53
  <ModalContent>
48
- <ModalHeader>
49
- <ModalTitle>Confirm action</ModalTitle>
50
- </ModalHeader>
51
-
52
- <ModalDescription>
53
- Are you sure you want to continue? This action cannot be undone.
54
- </ModalDescription>
55
-
56
- <ModalFooter>
57
- <Button appearance="secondary" onClick={() => setOpen(false)}>
58
- Cancel
59
- </Button>
60
-
61
- <ModalClose asChild>
62
- <Button>Confirm</Button>
63
- </ModalClose>
64
- </ModalFooter>
54
+ <Stack gap="sm">
55
+ <ModalHeader>
56
+ <ModalTitle>Confirm action</ModalTitle>
57
+ </ModalHeader>
58
+
59
+ <ModalDescription>
60
+ Are you sure you want to continue? This action cannot be undone.
61
+ </ModalDescription>
62
+
63
+ <ModalFooter>
64
+ <Button appearance="secondary" onClick={() => setOpen(false)}>
65
+ Cancel
66
+ </Button>
67
+
68
+ <ModalClose asChild>
69
+ <Button>Confirm</Button>
70
+ </ModalClose>
71
+ </ModalFooter>
72
+ </Stack>
73
+ </ModalContent>
74
+ </Modal>
75
+ );
76
+ },
77
+ };
78
+
79
+ export const WithFormContent: Story = {
80
+ parameters: {
81
+ docs: {
82
+ description: {
83
+ story:
84
+ "Arbitrary content (a FormGroup here, could be anything) sits in the same Stack as ModalHeader/ModalDescription/ModalFooter, getting the same gap on both sides automatically, no extra spacing rules needed regardless of what's between them.",
85
+ },
86
+ },
87
+ },
88
+ render: () => {
89
+ const [open, setOpen] = useState(false);
90
+
91
+ return (
92
+ <Modal open={open} onOpenChange={setOpen}>
93
+ <ModalTrigger asChild>
94
+ <Button>Open modal</Button>
95
+ </ModalTrigger>
96
+
97
+ <ModalContent>
98
+ <Stack gap="sm">
99
+ <ModalHeader>
100
+ <ModalTitle>Rename item</ModalTitle>
101
+ </ModalHeader>
102
+
103
+ <ModalDescription>Choose a new name for this item.</ModalDescription>
104
+
105
+ <FormGroup>
106
+ <Label htmlFor="rename-item">Name</Label>
107
+
108
+ <HintText>As it should appear in the list</HintText>
109
+
110
+ <Input id="rename-item" aria-describedby="rename-item-hint" defaultValue="Untitled" />
111
+ </FormGroup>
112
+
113
+ <ModalFooter>
114
+ <Button appearance="secondary" onClick={() => setOpen(false)}>
115
+ Cancel
116
+ </Button>
117
+
118
+ <ModalClose asChild>
119
+ <Button>Save</Button>
120
+ </ModalClose>
121
+ </ModalFooter>
122
+ </Stack>
65
123
  </ModalContent>
66
124
  </Modal>
67
125
  );
@@ -131,6 +131,15 @@ export const ModalTitle = ({ className, ...props }: ModalTitleProps) => (
131
131
 
132
132
  export type ModalDescriptionProps = ComponentProps<typeof DialogPrimitive.Description>;
133
133
 
134
+ // Renders a <p> (Description's own default), which can only contain
135
+ // phrasing content, children here is assumed to be plain descriptive text
136
+ // for that reason. Anything else (a Details/SummaryList section, a copy
137
+ // button, ...) belongs as a sibling of ModalDescription instead, not
138
+ // nested inside it, both because a <p> can't legally contain block
139
+ // content (a browser silently closes it early on hitting one, which
140
+ // desyncs SSR/client markup into a hydration error) and because
141
+ // aria-describedby content should read as plain description text, not
142
+ // fold in interactive elements. See ErrorModal for the sibling pattern.
134
143
  export const ModalDescription = ({ className, ...props }: ModalDescriptionProps) => (
135
144
  <DialogPrimitive.Description
136
145
  data-slot="modal-description"
@@ -100,6 +100,14 @@
100
100
  @apply panel absolute w-full mt-1 overflow-hidden cn-z-overlay;
101
101
  }
102
102
 
103
+ /* react-select flips the menu above the control when there isn't room
104
+ below. mt-1 pushes it further down rather than opening a gap in that
105
+ direction, mirror it as mb-1 so there's still a visible gap between
106
+ control and menu regardless of which side it opens on. */
107
+ .cn-select-menu-top {
108
+ @apply mt-0 mb-1;
109
+ }
110
+
103
111
  .cn-select-menu-list {
104
112
  @apply flex flex-col overflow-y-auto max-h-60;
105
113
  }
@@ -81,7 +81,12 @@ const buildClassNames = <Option, IsMulti extends boolean, Group extends GroupBas
81
81
  cn('cn-select-clear-indicator', userClassNames?.clearIndicator?.(props)),
82
82
  loadingIndicator: (props) =>
83
83
  cn('cn-select-loading-indicator', userClassNames?.loadingIndicator?.(props)),
84
- menu: (props) => cn('cn-select-menu', userClassNames?.menu?.(props)),
84
+ menu: (props) =>
85
+ cn(
86
+ 'cn-select-menu',
87
+ props.placement === 'top' && 'cn-select-menu-top',
88
+ userClassNames?.menu?.(props),
89
+ ),
85
90
  menuList: (props) => cn('cn-select-menu-list', userClassNames?.menuList?.(props)),
86
91
  option: (props) =>
87
92
  cn(
@@ -330,6 +330,7 @@ export { Header } from './Header/Header';
330
330
  export { type HeaderProps } from './Header/Header';
331
331
  export { HeaderAppContent } from './Header/HeaderAppContent';
332
332
  export { Footer } from './Footer/Footer';
333
+ export { type FooterProps } from './Footer/Footer';
333
334
  export { SmallFooter } from './Footer/Small/SmallFooter';
334
335
  export { Unauthorized } from './Layout/401/401';
335
336
  export { Forbidden } from './Layout/403/403';
@@ -13,6 +13,7 @@ export const MimeType = {
13
13
  Text: 'text/plain',
14
14
  NTriples: 'application/n-triples',
15
15
  OctetStream: 'application/octet-stream',
16
+ Mvt: 'application/vnd.mapbox-vector-tile',
16
17
  } as const;
17
18
 
18
19
  /**