@sakura-ui/sakura-ui 0.4.1 → 0.5.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/README.md +87 -0
- package/package.json +3 -3
- package/packages/core/package.json +1 -1
- package/packages/core/src/components/Card.tsx +51 -32
- package/packages/core/src/components/Faq.tsx +8 -24
- package/packages/core/src/components/LinkCard.tsx +103 -54
- package/packages/core/src/components/index.ts +1 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/tests/Card.test.tsx +124 -20
- package/packages/core/tests/Faq.test.tsx +75 -0
- package/packages/core/tests/LinkCard.test.tsx +151 -0
- package/packages/forms/.turbo/turbo-build.log +3 -3
- package/packages/forms/dist/index.cjs.js +18 -18
- package/packages/forms/dist/index.es.js +166 -178
- package/packages/forms/dist/types/components/Checkbox.d.ts.map +1 -1
- package/packages/forms/dist/types/components/FileInput.d.ts.map +1 -1
- package/packages/forms/dist/types/components/Input.d.ts.map +1 -1
- package/packages/forms/dist/types/components/Radio.d.ts.map +1 -1
- package/packages/forms/dist/types/components/Select.d.ts.map +1 -1
- package/packages/forms/dist/types/components/Textarea.d.ts.map +1 -1
- package/packages/forms/package.json +1 -1
- package/packages/forms/src/components/Checkbox.tsx +0 -1
- package/packages/forms/src/components/FileInput.tsx +3 -8
- package/packages/forms/src/components/Input.tsx +0 -1
- package/packages/forms/src/components/Radio.tsx +0 -3
- package/packages/forms/src/components/Select.tsx +0 -1
- package/packages/forms/src/components/Textarea.tsx +0 -1
- package/packages/forms/tests/FileInput.test.tsx +30 -5
- package/packages/helper/.turbo/turbo-build.log +1 -1
- package/packages/markdown/.turbo/turbo-build.log +4 -4
- package/packages/markdown/dist/index.cjs.js +23 -23
- package/packages/markdown/dist/index.es.js +1732 -1727
- package/packages/markdown/dist/types/components/Markdown.d.ts.map +1 -1
- package/packages/markdown/dist/types/plugins/card.d.ts.map +1 -1
- package/packages/markdown/package.json +3 -2
- package/packages/markdown/src/components/Markdown.tsx +35 -11
- package/packages/markdown/src/plugins/card.ts +14 -0
- package/packages/tailwind-theme-plugin/.turbo/turbo-build.log +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,13 @@ $ pnpm add @sakura-ui/core @sakura-ui/forms @sakura-ui/tailwind-theme-plugin @sa
|
|
|
25
25
|
$ pnpm add @sakura-ui/core@0.3.1 @sakura-ui/forms@0.2.2 @sakura-ui/tailwind-theme-plugin@0.2.2 @sakura-ui/markdown@0.0.17
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
### For the Card API before 0.5.0
|
|
29
|
+
`Card` and `LinkCard` changed shape in `@sakura-ui/core` 0.5.0. See
|
|
30
|
+
[Card and LinkCard](#card-and-linkcard) for what to change; to stay on the old API for now:
|
|
31
|
+
```
|
|
32
|
+
$ pnpm add @sakura-ui/core@0.4.1 @sakura-ui/markdown@0.2.2
|
|
33
|
+
```
|
|
34
|
+
|
|
28
35
|
## Configuration
|
|
29
36
|
tailwind.config.js
|
|
30
37
|
```ts
|
|
@@ -103,6 +110,40 @@ export default App
|
|
|
103
110
|
- Link
|
|
104
111
|
- Card
|
|
105
112
|
- LinkCard
|
|
113
|
+
|
|
114
|
+
### Card and LinkCard
|
|
115
|
+
`CardHeader` needs an `as` property. There is no default: the right level depends on the
|
|
116
|
+
document outline around the card, and a wrong guess breaks heading navigation. Use `p` for
|
|
117
|
+
lists of many cards, where headings would only add noise.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader as="h3">Title</CardHeader>
|
|
122
|
+
<CardBody>Body</CardBody>
|
|
123
|
+
</Card>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`LinkCard` puts the link in the title, and the link covers the whole card. Because of that a
|
|
127
|
+
link card cannot contain another link, a button or a form control.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<LinkCard>
|
|
131
|
+
<LinkCardHeader as="h3" href="/readme">Title</LinkCardHeader>
|
|
132
|
+
<CardBody>Body</CardBody>
|
|
133
|
+
<LinkCardFooter>June 27th, 2026</LinkCardFooter>
|
|
134
|
+
</LinkCard>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`Card` renders a `div` and carries no ARIA of its own, so nothing can dangle or collide. A
|
|
138
|
+
`div` maps to the generic role, which cannot take an accessible name. When a card really is a
|
|
139
|
+
self-contained composition, name it yourself:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<Card as="article" aria-labelledby="card-title">
|
|
143
|
+
<CardHeader as="h3" id="card-title">Title</CardHeader>
|
|
144
|
+
<CardBody>Body</CardBody>
|
|
145
|
+
</Card>
|
|
146
|
+
```
|
|
106
147
|
- Ol
|
|
107
148
|
- Ul
|
|
108
149
|
- Table
|
|
@@ -128,9 +169,55 @@ export default App
|
|
|
128
169
|
- LabelControl
|
|
129
170
|
- FieldsetControl
|
|
130
171
|
|
|
172
|
+
Every form control takes its accessible name from the label it is associated with, so wrap it in `LabelControl` — or `FieldsetControl` for a group of radios or checkboxes. A control rendered without a label has no name for assistive technology.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<LabelControl labelText="添付書類" isRequired>
|
|
176
|
+
<FileInput />
|
|
177
|
+
</LabelControl>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If a visible label is not an option, pass `aria-label` yourself; it is forwarded to the underlying element.
|
|
181
|
+
|
|
131
182
|
## Markdwon extension
|
|
132
183
|
- Markdown
|
|
133
184
|
|
|
185
|
+
## Accessible names
|
|
186
|
+
|
|
187
|
+
A note for anyone working on these components, rather than for people using them.
|
|
188
|
+
|
|
189
|
+
Whether an element can be named at all is decided by its role, not by the attribute
|
|
190
|
+
you write. Every ARIA role declares where its name may come from:
|
|
191
|
+
|
|
192
|
+
| Name from | Meaning | Roles |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| author, contents | `aria-label`, `aria-labelledby`, **or the text inside** | `button`, `link`, `heading`, `cell`, `option`, `tab`, `menuitem` |
|
|
195
|
+
| author | `aria-label`, `aria-labelledby` or `title` **only** | `article`, `region`, `dialog`, `img`, `table`, `form` |
|
|
196
|
+
| prohibited | cannot be named; the attribute is ignored | `generic` (a bare `<div>` or `<span>`), `paragraph` |
|
|
197
|
+
|
|
198
|
+
Two consequences catch people out.
|
|
199
|
+
|
|
200
|
+
**A `<div>` cannot be given a name.** It maps to `generic`, where naming is prohibited,
|
|
201
|
+
so `<div aria-label="Card">` does nothing at all. Reaching for a name means reaching for
|
|
202
|
+
a different element, which is why dropping `aria-labelledby` from `Card` and dropping its
|
|
203
|
+
`<article>` were the same decision.
|
|
204
|
+
|
|
205
|
+
**An `<article>` does not take its name from a heading inside it.** It is named by the
|
|
206
|
+
author only, so `<article><h3>Title</h3></article>` has no accessible name. A `<button>`
|
|
207
|
+
or an `<a>` would be named "Title" here, because those are named from their contents.
|
|
208
|
+
Mixing the two up is what made `LinkCard` produce a link with no name: an `<a>` wrapping
|
|
209
|
+
an `<article>` never reached the title, because Chrome does not descend into a role that
|
|
210
|
+
is named by its author.
|
|
211
|
+
|
|
212
|
+
So, in practice:
|
|
213
|
+
|
|
214
|
+
- Leave it a `<div>` and give it no name. A list and the headings inside it already tell
|
|
215
|
+
the reader where they are.
|
|
216
|
+
- Use `<section>` only with a name. A `<section>` becomes a `region` **because** it has one;
|
|
217
|
+
without a name it is no different from a `<div>`.
|
|
218
|
+
- Use `<article>` only for something that stands on its own, and name it when you do.
|
|
219
|
+
Naming it is recommended by the APG so that readers can tell one article from the next.
|
|
220
|
+
|
|
134
221
|
## Development
|
|
135
222
|
|
|
136
223
|
### Setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sakura-ui/sakura-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"author": "glassonion1",
|
|
6
6
|
"homepage": "https://github.com/glassonion1/sakura-ui",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"pnpm": ">=9.15.1"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@sakura-ui/
|
|
22
|
+
"@sakura-ui/forms": "0.4.2",
|
|
23
|
+
"@sakura-ui/core": "0.5.0",
|
|
23
24
|
"@sakura-ui/helper": "0.0.9",
|
|
24
|
-
"@sakura-ui/forms": "0.4.1",
|
|
25
25
|
"@sakura-ui/tailwind-theme-plugin": "0.4.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { cx } from '@sakura-ui/helper'
|
|
3
3
|
|
|
4
|
-
interface IdContextType {
|
|
5
|
-
id: string
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const IdContext = React.createContext<IdContextType>({ id: '' })
|
|
9
|
-
|
|
10
4
|
export namespace Card {
|
|
11
|
-
export interface Props extends React.
|
|
5
|
+
export interface Props<T extends React.ElementType> {
|
|
6
|
+
as?: T
|
|
7
|
+
}
|
|
12
8
|
}
|
|
13
9
|
|
|
14
|
-
export const Card =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const ctx: IdContextType = { id: id }
|
|
10
|
+
export const Card = <T extends React.ElementType = 'div'>(
|
|
11
|
+
props: Card.Props<T> & Omit<React.ComponentProps<T>, keyof Card.Props<T>>
|
|
12
|
+
) => {
|
|
13
|
+
const { as: Component = 'div', className, children, ...restProps } = props
|
|
19
14
|
|
|
20
15
|
const style = `
|
|
21
16
|
border
|
|
@@ -27,20 +22,17 @@ export const Card = (props: Card.Props) => {
|
|
|
27
22
|
overflow-hidden
|
|
28
23
|
`
|
|
29
24
|
|
|
25
|
+
// A div cannot carry an accessible name, so no ARIA is set here. See the
|
|
26
|
+
// Accessible names section of the README before adding any.
|
|
30
27
|
return (
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
aria-describedby={`${ctx.id}-desc`}
|
|
35
|
-
className={cx(style, className)}
|
|
36
|
-
{...restProps}
|
|
37
|
-
>
|
|
38
|
-
{children}
|
|
39
|
-
</article>
|
|
40
|
-
</IdContext>
|
|
28
|
+
<Component className={cx(style, className)} {...restProps}>
|
|
29
|
+
{children}
|
|
30
|
+
</Component>
|
|
41
31
|
)
|
|
42
32
|
}
|
|
43
33
|
|
|
34
|
+
Card.displayName = 'Card'
|
|
35
|
+
|
|
44
36
|
export namespace CardImg {
|
|
45
37
|
export interface Props extends React.ComponentPropsWithoutRef<'img'> {}
|
|
46
38
|
}
|
|
@@ -57,16 +49,37 @@ export const CardImg = (props: CardImg.Props) => {
|
|
|
57
49
|
return <img className={cx(style, className)} {...restProps} />
|
|
58
50
|
}
|
|
59
51
|
|
|
52
|
+
CardImg.displayName = 'CardImg'
|
|
53
|
+
|
|
54
|
+
export type CardHeaderAs = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'
|
|
55
|
+
|
|
60
56
|
export namespace CardHeader {
|
|
61
|
-
export interface Props extends React.ComponentPropsWithoutRef<'
|
|
57
|
+
export interface Props extends React.ComponentPropsWithoutRef<'h3'> {
|
|
58
|
+
/**
|
|
59
|
+
* The element to render the title as. Required on purpose: the right level
|
|
60
|
+
* depends on the surrounding document outline, so the library must not pick
|
|
61
|
+
* one. Use 'p' for lists of many cards, where headings would add noise.
|
|
62
|
+
*/
|
|
63
|
+
as: CardHeaderAs
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
export const CardHeader = (props: CardHeader.Props) => {
|
|
65
|
-
const { className, children, ...restProps } = props
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const { as: Component, className, children, ...restProps } = props
|
|
69
|
+
|
|
70
|
+
if (!Component) {
|
|
71
|
+
// Without this, React reports an invalid element type and suggests a missing
|
|
72
|
+
// export, which says nothing about the property that was actually left out.
|
|
73
|
+
throw new Error(
|
|
74
|
+
'CardHeader: the "as" property is required. Pass the heading level that ' +
|
|
75
|
+
'fits the outline around the card, or "p" for lists of many cards.'
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Reset the browser defaults of the heading and paragraph elements so that the
|
|
80
|
+
// rendered element does not change how the card looks.
|
|
69
81
|
const style = `
|
|
82
|
+
m-0
|
|
70
83
|
text-base
|
|
71
84
|
leading-[2rem]
|
|
72
85
|
font-medium
|
|
@@ -77,12 +90,14 @@ export const CardHeader = (props: CardHeader.Props) => {
|
|
|
77
90
|
`
|
|
78
91
|
|
|
79
92
|
return (
|
|
80
|
-
<
|
|
93
|
+
<Component className={cx(style, className)} {...restProps}>
|
|
81
94
|
{children}
|
|
82
|
-
</
|
|
95
|
+
</Component>
|
|
83
96
|
)
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
CardHeader.displayName = 'CardHeader'
|
|
100
|
+
|
|
86
101
|
export namespace CardBody {
|
|
87
102
|
export interface Props extends React.ComponentPropsWithoutRef<'div'> {}
|
|
88
103
|
}
|
|
@@ -90,8 +105,6 @@ export namespace CardBody {
|
|
|
90
105
|
export const CardBody = (props: CardBody.Props) => {
|
|
91
106
|
const { className, children, ...restProps } = props
|
|
92
107
|
|
|
93
|
-
const ctx = React.useContext(IdContext)
|
|
94
|
-
|
|
95
108
|
const style = `
|
|
96
109
|
text-base-sm
|
|
97
110
|
leading-[1.85rem]
|
|
@@ -101,13 +114,17 @@ export const CardBody = (props: CardBody.Props) => {
|
|
|
101
114
|
px-6
|
|
102
115
|
`
|
|
103
116
|
|
|
117
|
+
// No generated id here. Callers that want the body to describe the card pass
|
|
118
|
+
// their own id and point aria-describedby at it.
|
|
104
119
|
return (
|
|
105
|
-
<div
|
|
120
|
+
<div className={cx(style, className)} {...restProps}>
|
|
106
121
|
{children}
|
|
107
122
|
</div>
|
|
108
123
|
)
|
|
109
124
|
}
|
|
110
125
|
|
|
126
|
+
CardBody.displayName = 'CardBody'
|
|
127
|
+
|
|
111
128
|
export namespace CardFooter {
|
|
112
129
|
export interface Props extends React.ComponentPropsWithoutRef<'div'> {}
|
|
113
130
|
}
|
|
@@ -135,3 +152,5 @@ export const CardFooter = (props: CardFooter.Props) => {
|
|
|
135
152
|
</div>
|
|
136
153
|
)
|
|
137
154
|
}
|
|
155
|
+
|
|
156
|
+
CardFooter.displayName = 'CardFooter'
|
|
@@ -18,18 +18,12 @@ export const Faq = (props: Faq.Props) => {
|
|
|
18
18
|
flex-col
|
|
19
19
|
gap-8
|
|
20
20
|
`
|
|
21
|
+
// No schema.org FAQPage markup here. Google discontinued the FAQ rich result
|
|
22
|
+
// in May 2026, so it is not implemented.
|
|
21
23
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
itemScope
|
|
26
|
-
itemProp="mainEntity"
|
|
27
|
-
itemType="https://schema.org/Question"
|
|
28
|
-
{...restProps}
|
|
29
|
-
>
|
|
30
|
-
{children}
|
|
31
|
-
</dl>
|
|
32
|
-
</article>
|
|
24
|
+
<dl className={cx(style, className)} {...restProps}>
|
|
25
|
+
{children}
|
|
26
|
+
</dl>
|
|
33
27
|
)
|
|
34
28
|
}
|
|
35
29
|
|
|
@@ -47,11 +41,7 @@ export const Question = (props: Question.Props) => {
|
|
|
47
41
|
mt-8
|
|
48
42
|
`
|
|
49
43
|
return (
|
|
50
|
-
<dt
|
|
51
|
-
className={cx(style, headingStyle, className)}
|
|
52
|
-
itemProp="name"
|
|
53
|
-
{...restProps}
|
|
54
|
-
>
|
|
44
|
+
<dt className={cx(style, headingStyle, className)} {...restProps}>
|
|
55
45
|
<span aria-hidden="true">Q</span>
|
|
56
46
|
<span>{children}</span>
|
|
57
47
|
</dt>
|
|
@@ -72,17 +62,11 @@ export const Answer = (props: Answer.Props) => {
|
|
|
72
62
|
gap-8
|
|
73
63
|
`
|
|
74
64
|
return (
|
|
75
|
-
<dd
|
|
76
|
-
className={cx(style, className)}
|
|
77
|
-
itemScope
|
|
78
|
-
itemProp="acceptedAnswer"
|
|
79
|
-
itemType="https://schema.org/Answer"
|
|
80
|
-
{...restProps}
|
|
81
|
-
>
|
|
65
|
+
<dd className={cx(style, className)} {...restProps}>
|
|
82
66
|
<span className={cx(headingStyle, '!leading-none')} aria-hidden="true">
|
|
83
67
|
A
|
|
84
68
|
</span>
|
|
85
|
-
<span
|
|
69
|
+
<span>{children}</span>
|
|
86
70
|
</dd>
|
|
87
71
|
)
|
|
88
72
|
}
|
|
@@ -1,98 +1,145 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { cx
|
|
3
|
-
import { Card, CardFooter, CardHeader } from './Card'
|
|
2
|
+
import { cx } from '@sakura-ui/helper'
|
|
3
|
+
import { Card, CardFooter, CardHeader, type CardHeaderAs } from './Card'
|
|
4
4
|
import { Icon } from './Icon'
|
|
5
5
|
|
|
6
6
|
export namespace LinkCard {
|
|
7
|
-
export interface Props<T extends React.ElementType>
|
|
8
|
-
extends React.ComponentProps<'article'> {
|
|
9
|
-
href: string
|
|
10
|
-
target?: string
|
|
11
|
-
linkAs?: T
|
|
12
|
-
}
|
|
7
|
+
export interface Props<T extends React.ElementType> extends Card.Props<T> {}
|
|
13
8
|
}
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A Card whose title carries the link. The link stretches over the whole card
|
|
12
|
+
* through a ::after overlay, so the card cannot contain another link, a button
|
|
13
|
+
* or a form control.
|
|
14
|
+
*/
|
|
15
|
+
export const LinkCard = <T extends React.ElementType = 'div'>(
|
|
16
|
+
props: LinkCard.Props<T> & Omit<React.ComponentProps<T>, keyof LinkCard.Props<T>>
|
|
17
17
|
) => {
|
|
18
|
-
const {
|
|
19
|
-
href,
|
|
20
|
-
target,
|
|
21
|
-
linkAs: Component = 'a',
|
|
22
|
-
className,
|
|
23
|
-
children,
|
|
24
|
-
...rest
|
|
25
|
-
} = props
|
|
18
|
+
const { className, children, ...restProps } = props
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
// z-0 establishes a stacking context so that the overlay of the title can be
|
|
21
|
+
// raised above the rest of the card. CardFooter is sticky, which makes it a
|
|
22
|
+
// positioned element that would otherwise paint over the overlay and swallow
|
|
23
|
+
// the clicks aimed at the bottom of the card.
|
|
24
|
+
const stylePosition = `
|
|
25
|
+
relative
|
|
26
|
+
z-0
|
|
34
27
|
group
|
|
35
28
|
`
|
|
36
29
|
|
|
37
30
|
const styleHover = `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
hover:outline
|
|
32
|
+
hover:outline-2
|
|
33
|
+
hover:outline-black
|
|
34
|
+
hover:bg-solid-gray-50
|
|
42
35
|
`
|
|
43
36
|
|
|
37
|
+
// The focus ring goes on the card itself, not on the <a>, so that it surrounds
|
|
38
|
+
// the whole card. The card sets overflow-hidden, which would cut off a ring
|
|
39
|
+
// drawn on the ::after overlay inside it. An element never clips its own
|
|
40
|
+
// outline, so the ring survives here.
|
|
41
|
+
const styleFocus = `
|
|
42
|
+
has-[a:focus-visible]:outline
|
|
43
|
+
has-[a:focus-visible]:outline-4
|
|
44
|
+
has-[a:focus-visible]:outline-black
|
|
45
|
+
has-[a:focus-visible]:outline-offset-[calc(2/16*1rem)]
|
|
46
|
+
has-[a:focus-visible]:ring-[calc(2/16*1rem)]
|
|
47
|
+
has-[a:focus-visible]:ring-yellow-300
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
// TypeScript cannot carry the generic through to Card, which is polymorphic in
|
|
51
|
+
// the same way. The public signature above stays exact; only this hand-off is
|
|
52
|
+
// widened.
|
|
53
|
+
const Root = Card as React.ElementType
|
|
54
|
+
|
|
44
55
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
<Root
|
|
57
|
+
className={cx(stylePosition, styleHover, styleFocus, className)}
|
|
58
|
+
{...restProps}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</Root>
|
|
50
62
|
)
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
LinkCard.displayName = 'LinkCard'
|
|
66
|
+
|
|
53
67
|
export namespace LinkCardHeader {
|
|
54
|
-
export interface Props extends React.
|
|
55
|
-
|
|
68
|
+
export interface Props<T extends React.ElementType> {
|
|
69
|
+
/** The element to render the title as. See CardHeader. */
|
|
70
|
+
as: CardHeaderAs
|
|
71
|
+
/** The component to render the link as. Defaults to an anchor element. */
|
|
72
|
+
linkAs?: T
|
|
56
73
|
}
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The title of a LinkCard, holding the link that covers the whole card.
|
|
78
|
+
*
|
|
79
|
+
* `className` styles the title element; every other prop (href, target, and
|
|
80
|
+
* whatever the component passed to `linkAs` accepts) goes to the link.
|
|
81
|
+
*/
|
|
82
|
+
export const LinkCardHeader = <T extends React.ElementType = 'a'>(
|
|
83
|
+
props: LinkCardHeader.Props<T> &
|
|
84
|
+
Omit<React.ComponentProps<T>, keyof LinkCardHeader.Props<T>>
|
|
85
|
+
) => {
|
|
86
|
+
const {
|
|
87
|
+
as,
|
|
88
|
+
linkAs: Component = 'a',
|
|
89
|
+
className,
|
|
90
|
+
children,
|
|
91
|
+
...restProps
|
|
92
|
+
} = props
|
|
93
|
+
|
|
94
|
+
if (!as) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'LinkCardHeader: the "as" property is required. Pass the heading level ' +
|
|
97
|
+
'that fits the outline around the card, or "p" for lists of many cards.'
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
64
101
|
const styleHeading = `
|
|
65
102
|
decoration-blue-900
|
|
66
103
|
underline
|
|
67
104
|
underline-offset-[calc(3/16*1rem)]
|
|
68
|
-
|
|
69
|
-
const styleHover = `
|
|
105
|
+
|
|
70
106
|
group-hover:text-blue-900
|
|
71
107
|
group-hover:decoration-[calc(3/16*1rem)]
|
|
72
108
|
`
|
|
73
109
|
|
|
110
|
+
// The ::after overlay makes the whole card clickable. It carries no visual of
|
|
111
|
+
// its own; the focus ring lives on the LinkCard element.
|
|
112
|
+
const styleOverlay = `
|
|
113
|
+
after:content-['']
|
|
114
|
+
after:absolute
|
|
115
|
+
after:inset-0
|
|
116
|
+
after:z-10
|
|
117
|
+
focus-visible:outline-none
|
|
118
|
+
`
|
|
119
|
+
|
|
74
120
|
return (
|
|
75
|
-
<CardHeader className={cx(className, styleHeading
|
|
76
|
-
<
|
|
121
|
+
<CardHeader as={as} className={cx(className, styleHeading)}>
|
|
122
|
+
<Component className={styleOverlay} {...restProps}>
|
|
77
123
|
{children}
|
|
78
|
-
{target === '_blank' && (
|
|
79
|
-
<Icon opticalSize={16} className="ml-1">
|
|
124
|
+
{restProps.target === '_blank' && (
|
|
125
|
+
<Icon opticalSize={16} className="ml-1" altText="新しいタブで開きます">
|
|
80
126
|
open_in_new
|
|
81
127
|
</Icon>
|
|
82
128
|
)}
|
|
83
|
-
</
|
|
129
|
+
</Component>
|
|
84
130
|
</CardHeader>
|
|
85
131
|
)
|
|
86
132
|
}
|
|
87
133
|
|
|
134
|
+
LinkCardHeader.displayName = 'LinkCardHeader'
|
|
135
|
+
|
|
88
136
|
export namespace LinkCardFooter {
|
|
89
|
-
export interface Props extends React.
|
|
137
|
+
export interface Props extends React.ComponentPropsWithoutRef<'div'> {}
|
|
90
138
|
}
|
|
91
139
|
|
|
92
|
-
export const LinkCardFooter = ({
|
|
93
|
-
className,
|
|
94
|
-
|
|
95
|
-
}: LinkCardFooter.Props) => {
|
|
140
|
+
export const LinkCardFooter = (props: LinkCardFooter.Props) => {
|
|
141
|
+
const { className, children, ...restProps } = props
|
|
142
|
+
|
|
96
143
|
const style = `
|
|
97
144
|
flex
|
|
98
145
|
justify-between
|
|
@@ -116,7 +163,7 @@ export const LinkCardFooter = ({
|
|
|
116
163
|
`
|
|
117
164
|
|
|
118
165
|
return (
|
|
119
|
-
<CardFooter className={cx(
|
|
166
|
+
<CardFooter className={cx(style, className)} {...restProps}>
|
|
120
167
|
<span>{children}</span>
|
|
121
168
|
<span className={cx(styleIcon, styleIconHover)}>
|
|
122
169
|
<Icon opticalSize={16}>arrow_forward</Icon>
|
|
@@ -124,3 +171,5 @@ export const LinkCardFooter = ({
|
|
|
124
171
|
</CardFooter>
|
|
125
172
|
)
|
|
126
173
|
}
|
|
174
|
+
|
|
175
|
+
LinkCardFooter.displayName = 'LinkCardFooter'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Button } from './Button'
|
|
2
2
|
export { MenuButton } from './MenuButton'
|
|
3
3
|
export { Card, CardImg, CardHeader, CardBody, CardFooter } from './Card'
|
|
4
|
+
export type { CardHeaderAs } from './Card'
|
|
4
5
|
export { LinkCard, LinkCardHeader, LinkCardFooter } from './LinkCard'
|
|
5
6
|
export { Code } from './Code'
|
|
6
7
|
export { Faq, Question, Answer } from './Faq'
|