nene-lang-ui 0.0.0-stage → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kimuracoki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # nene-lang-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Nene](https://github.com/kimuracoki/nene) の公式 UI 部品集。ボタン・入力欄・ダイアログ・メニューなど 20 の部品がそろっている。ライトとダークの両方に対応し、色は CSS 変数で変えられる。
4
+
5
+ `nene new` で作ったプロジェクトには、はじめから入っている。あとから入れるときは:
6
+
7
+ ```sh
8
+ npm install nene-lang-ui
9
+ ```
10
+
11
+ ```haskell
12
+ import Ui.Button (Variant(..))
13
+ import qualified Ui.Icon as Icon
14
+ import Ui.Theme (Tone(..))
15
+
16
+ <Button onClick={Save}>保存</Button>
17
+ <Button variant={Outlined} tone={Danger} startIcon={Just Icon.delete} onClick={Remove}>削除</Button>
18
+ ```
19
+
20
+ 全部の部品を使った見本は、Nene のリポジトリの [examples/Showcase.nn](https://github.com/kimuracoki/nene/blob/main/examples/Showcase.nn)。
21
+
22
+ ## 部品
23
+
24
+ | モジュール | タグ | 主な属性 |
25
+ | --- | --- | --- |
26
+ | `Ui.Button` | `<Button>…</Button>` | `onClick`、`variant`(`Filled` `Outlined` `Plain`)、`tone`、`size`、`startIcon` `endIcon`、`disabled`、`fullWidth` |
27
+ | `Ui.IconButton` | `<IconButton />` | `icon`、`label`(読み上げとツールチップ)、`onClick`、`variant`、`tone`、`size` |
28
+ | `Ui.Icon` | `<Icon />` | `name`。`Icon.add` `Icon.close` `Icon.search` など 30 種類 |
29
+ | `Ui.TextField` | `<TextField />` | `value`、`onInput`、`label`、`placeholder`、`helper`、`error`、`kind`(`PlainText` `Email` `Password` `Number` `Search` `Tel`) |
30
+ | `Ui.Select` | `<Select />` | `value`、`options`(値と表示の組のリスト)、`onChange`、`label` |
31
+ | `Ui.Checkbox` | `<Checkbox />` | `checked`、`onCheck`、`label` |
32
+ | `Ui.Switch` | `<Switch />` | `checked`、`onCheck`、`label` |
33
+ | `Ui.Card` | `<Card>…</Card>` | `title`、`padding`(`None` 〜 `XL`) |
34
+ | `Ui.Chip` | `<Chip>…</Chip>` | `tone`、`selected`、`onClick`(`Just msg` で押せるチップになる) |
35
+ | `Ui.Dialog` | `<Dialog>…</Dialog>` | `open`、`onClose`、`title` |
36
+ | `Ui.Tabs` | `<Tabs />` | `labels`、`selected`(0 から)、`onSelect` |
37
+ | `Ui.Alert` | `<Alert>…</Alert>` | `tone`、`title` |
38
+ | `Ui.Progress` | `<Progress />` | `value`(`Just 0.4`。`Nothing` なら動き続ける)、`tone` |
39
+ | `Ui.Spinner` | `<Spinner />` | `size`、`label` |
40
+ | `Ui.Avatar` | `<Avatar />` | `name`(頭文字を出す)、`src`、`size` |
41
+ | `Ui.Badge` | `<Badge>…</Badge>` | `count`(0 なら出さない)、`max`、`tone` |
42
+ | `Ui.Menu` | `<Menu />` | `id`、`label`、`items`(`Item { label, onClick, icon, tone }` のリスト)、`variant` |
43
+ | `Ui.Popover` | `<Popover>…</Popover>` | `id`、`label`、`variant`。中身は自由 |
44
+ | `Ui.Accordion` | `<Accordion>…</Accordion>` | `title`、`open`(最初から開くか)、`group`(同じ group は 1 つだけ開く) |
45
+ | `Ui.Tooltip` | `<Tooltip>…</Tooltip>` | `text` |
46
+
47
+ - 色の種類 `tone` は、どの部品でも `Ui.Theme` の `Primary` `Neutral` `Success` `Warning` `Danger`。
48
+ - 大きさ `size` は、レイアウトタグと同じ `S` `M` `L`。
49
+ - アプリが知る必要のある状態(入力した値、選んだタブ、Dialog を開いているか)は model に持つ。
50
+ - Menu・Popover・Accordion・Tooltip の開け閉めはブラウザが受け持つので、model に何も足さなくてよい。Menu と Popover の `id` は、ページの中で重ならない名前にする。
51
+
52
+ ## 色と形を変える
53
+
54
+ 部品の見た目は、部品の外から `class` で上書きできない(Nene の規則)。変えられるのは CSS 変数だけで、アプリの入口のファイルの `style` ブロックに書く。
55
+
56
+ ```css
57
+ style
58
+ :root {
59
+ --primary: #e11d48;
60
+ --radius: 6px;
61
+ }
62
+ ```
63
+
64
+ | 変数 | 意味 |
65
+ | --- | --- |
66
+ | `--primary` `--neutral` `--success` `--warning` `--danger` | 色の種類ごとの色 |
67
+ | `--on-tone` | 色の上に載せる文字の色 |
68
+ | `--text` `--muted` | 文字の色、控えめな文字の色 |
69
+ | `--surface` `--background` `--border` | 面、背景、枠線の色 |
70
+ | `--radius` `--shadow` | 角の丸み、影 |
71
+
72
+ 既定の値は `light-dark()` でライトとダークの両方を持つ([src/Ui/Theme.nn](src/Ui/Theme.nn))。ダークにしないときは `:root { color-scheme: light; }`。
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "nene-lang-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Nene の公式 UI 部品集。ボタン・入力欄・ダイアログなど",
5
+ "type": "module",
6
+ "nene": {
7
+ "source": "src"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "peerDependencies": {
14
+ "nene-lang": "^0.1.2"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "peerDependenciesMeta": {
20
+ "nene-lang": {
21
+ "optional": true
22
+ }
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/kimuracoki/nene.git",
27
+ "directory": "ui"
28
+ },
29
+ "homepage": "https://github.com/kimuracoki/nene/tree/main/ui#readme",
30
+ "license": "MIT",
31
+ "author": "kimuracoki",
32
+ "keywords": [
33
+ "nene",
34
+ "ui",
35
+ "components"
36
+ ]
37
+ }
@@ -0,0 +1,43 @@
1
+ -- 見出しを押すと開け閉めできる枠。開け閉めはブラウザが受け持つので、model に何も足さなくてよい
2
+ --
3
+ -- <Accordion title="よくある質問">…</Accordion>
4
+ -- <Accordion title="支払い" group="faq">…</Accordion> -- 同じ group のものは、1 つだけが開く
5
+ module Ui.Accordion (AccordionProps, accordion) where
6
+
7
+ import qualified Ui.Icon as Icon
8
+
9
+
10
+ type AccordionProps =
11
+ { title :: String
12
+ , open :: Bool = False -- 最初から開いておくか
13
+ , group :: String = ""
14
+ }
15
+
16
+
17
+ accordion :: AccordionProps -> [Html msg] -> Html msg
18
+ accordion props children =
19
+ <details class={css.accordion} open={props.open} name={props.group}>
20
+ <summary class={css.summary}>
21
+ <Row gap={S} align={Center}>
22
+ <span class={css.title}>{props.title}</span>
23
+ <Spacer />
24
+ <span class={css.chevron}><Icon name={Icon.chevronDown} /></span>
25
+ </Row>
26
+ </summary>
27
+ <div class={css.body}>{children}</div>
28
+ </details>
29
+
30
+
31
+ style
32
+ .accordion { border-bottom: 1px solid var(--border); }
33
+ .summary {
34
+ padding: 12px 4px;
35
+ list-style: none;
36
+ cursor: pointer;
37
+ }
38
+ .summary::-webkit-details-marker { display: none; }
39
+ .summary:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 35%, transparent); border-radius: 4px; }
40
+ .title { font-weight: 600; }
41
+ .chevron { display: inline-flex; color: var(--muted); transition: transform .2s; }
42
+ .accordion[open] .chevron { transform: rotate(180deg); }
43
+ .body { padding: 0 4px 16px; color: var(--muted); }
@@ -0,0 +1,53 @@
1
+ -- 知らせる枠。色の種類に合ったアイコンが付く
2
+ --
3
+ -- <Alert tone={Success} title="保存しました">次回からこの設定で開きます。</Alert>
4
+ module Ui.Alert (AlertProps, alert) where
5
+
6
+ import qualified Ui.Icon as Icon
7
+ import Ui.Theme (Tone(..), toneVars)
8
+
9
+
10
+ type AlertProps =
11
+ { tone :: Tone = Primary
12
+ , title :: String = ""
13
+ }
14
+
15
+
16
+ alert :: AlertProps -> [Html msg] -> Html msg
17
+ alert props children =
18
+ <div class={css.alert} vars={colors} role="status">
19
+ <Row gap={S}>
20
+ <span class={css.mark}><Icon name={symbol props.tone} /></span>
21
+ <Column gap={XS}>
22
+ <Heading text={props.title} />
23
+ <div class={css.body}>{children}</div>
24
+ </Column>
25
+ </Row>
26
+ </div>
27
+ where
28
+ colors = toneVars props.tone
29
+
30
+
31
+ heading :: { text :: String } -> Html msg
32
+ heading { text = "" } = <></>
33
+ heading { text } = <strong class={css.title}>{text}</strong>
34
+
35
+
36
+ symbol :: Tone -> Icon.Icon
37
+ symbol Primary = Icon.info
38
+ symbol Neutral = Icon.info
39
+ symbol Success = Icon.success
40
+ symbol Warning = Icon.warning
41
+ symbol Danger = Icon.error
42
+
43
+
44
+ style
45
+ .alert {
46
+ padding: 12px 16px;
47
+ border: 1px solid color-mix(in srgb, var(--tone) 30%, transparent);
48
+ border-radius: var(--radius);
49
+ background: color-mix(in srgb, var(--tone) 8%, var(--surface));
50
+ }
51
+ .mark { color: var(--tone); line-height: 1.5; }
52
+ .title { color: var(--tone); }
53
+ .body { font-size: .9375rem; }
@@ -0,0 +1,49 @@
1
+ -- 人を表す丸い印。画像がなければ名前の頭文字を出す
2
+ --
3
+ -- <Avatar name="Ada Lovelace" />
4
+ -- <Avatar name="Ada Lovelace" src={Just user.photo} size={L} />
5
+ module Ui.Avatar (AvatarProps, avatar) where
6
+
7
+
8
+ type AvatarProps =
9
+ { name :: String
10
+ , src :: Maybe String = Nothing
11
+ , size :: Size = M
12
+ }
13
+
14
+
15
+ avatar :: AvatarProps -> Html msg
16
+ avatar props = <Face photo={props.src} />
17
+ where
18
+ look = classes [css.avatar, sizeClass props.size]
19
+ face { photo = Just url } = <img class={look} src={url} alt={props.name} />
20
+ face { photo = Nothing } = <span class={look} title={props.name} aria-label={props.name}>{initials props.name}</span>
21
+
22
+
23
+ -- "Ada Lovelace" は "AL"、"木村" は "木"
24
+ initials :: String -> String
25
+ initials name = String.toUpper (String.join "" (map (String.slice 0 1) (take 2 (words name))))
26
+
27
+
28
+ sizeClass :: Size -> Class
29
+ sizeClass XS = css.small
30
+ sizeClass S = css.small
31
+ sizeClass L = css.large
32
+ sizeClass XL = css.large
33
+ sizeClass _ = css.medium
34
+
35
+
36
+ style
37
+ .avatar {
38
+ display: inline-grid;
39
+ flex: none;
40
+ place-items: center;
41
+ border-radius: 50%;
42
+ object-fit: cover;
43
+ background: color-mix(in srgb, var(--primary) 18%, var(--surface));
44
+ color: var(--primary);
45
+ font-weight: 700;
46
+ }
47
+ .small { width: 28px; height: 28px; font-size: .75rem; }
48
+ .medium { width: 40px; height: 40px; font-size: .9375rem; }
49
+ .large { width: 64px; height: 64px; font-size: 1.375rem; }
@@ -0,0 +1,50 @@
1
+ -- 子要素の右上に数を重ねる。count が 0 のときは出さない
2
+ --
3
+ -- <Badge count={model.unread}>
4
+ -- <IconButton icon={Icon.mail} label="メール" onClick={OpenMail} />
5
+ -- </Badge>
6
+ module Ui.Badge (BadgeProps, badge) where
7
+
8
+ import Ui.Theme (Tone(..), toneVars)
9
+
10
+
11
+ type BadgeProps =
12
+ { count :: Int
13
+ , max :: Int = 99 -- これを超えたら 99+ と出す
14
+ , tone :: Tone = Danger
15
+ }
16
+
17
+
18
+ badge :: BadgeProps -> [Html msg] -> Html msg
19
+ badge props children =
20
+ <span class={css.anchor}>
21
+ {children}
22
+ <Count n={props.count} />
23
+ </span>
24
+ where
25
+ colors = toneVars props.tone
26
+ count { n = 0 } = <></>
27
+ count { n } = <span class={css.badge} vars={colors}>{label n}</span>
28
+ label n
29
+ | n > props.max = show props.max ++ "+"
30
+ | otherwise = show n
31
+
32
+
33
+ style
34
+ .anchor { position: relative; display: inline-flex; }
35
+ .badge {
36
+ position: absolute;
37
+ top: -4px;
38
+ right: -4px;
39
+ min-width: 18px;
40
+ height: 18px;
41
+ padding: 0 5px;
42
+ border: 2px solid var(--surface);
43
+ border-radius: 999px;
44
+ background: var(--tone);
45
+ color: var(--on-tone);
46
+ font-size: .6875rem;
47
+ font-weight: 700;
48
+ line-height: 14px;
49
+ text-align: center;
50
+ }
@@ -0,0 +1,97 @@
1
+ -- ボタン
2
+ --
3
+ -- <Button onClick={Save}>保存</Button>
4
+ -- <Button variant={Outlined} tone={Danger} startIcon={Just Icon.delete} onClick={Remove}>削除</Button>
5
+ module Ui.Button (ButtonProps, Variant(..), button) where
6
+
7
+ import qualified Ui.Icon as Icon
8
+ import Ui.Theme (Tone(..), toneVars)
9
+
10
+
11
+ data Variant = Filled | Outlined | Plain
12
+
13
+ type ButtonProps msg =
14
+ { onClick :: msg
15
+ , variant :: Variant = Filled
16
+ , tone :: Tone = Primary
17
+ , size :: Size = M
18
+ , startIcon :: Maybe Icon.Icon = Nothing
19
+ , endIcon :: Maybe Icon.Icon = Nothing
20
+ , disabled :: Bool = False
21
+ , fullWidth :: Bool = False
22
+ }
23
+
24
+
25
+ button :: ButtonProps msg -> [Html msg] -> Html msg
26
+ button props children =
27
+ <button class={look} vars={colors} type="button" disabled={props.disabled} onClick={props.onClick}>
28
+ <Row gap={S} align={Center} justify={Center}>
29
+ <Slot icon={props.startIcon} />
30
+ {children}
31
+ <Slot icon={props.endIcon} />
32
+ </Row>
33
+ </button>
34
+ where
35
+ colors = toneVars props.tone
36
+ look = classes [css.button, variantClass props.variant, sizeClass props.size, width]
37
+ width
38
+ | props.fullWidth = css.full
39
+ | otherwise = classes []
40
+
41
+
42
+ slot :: { icon :: Maybe Icon.Icon } -> Html msg
43
+ slot { icon = Just i } = <Icon name={i} />
44
+ slot { icon = Nothing } = <></>
45
+
46
+
47
+ variantClass :: Variant -> Class
48
+ variantClass Filled = css.filled
49
+ variantClass Outlined = css.outlined
50
+ variantClass Plain = css.plain
51
+
52
+ sizeClass :: Size -> Class
53
+ sizeClass XS = css.small
54
+ sizeClass S = css.small
55
+ sizeClass L = css.large
56
+ sizeClass XL = css.large
57
+ sizeClass _ = css.medium
58
+
59
+
60
+ style
61
+ .button {
62
+ border: 1px solid transparent;
63
+ border-radius: var(--radius);
64
+ font-weight: 600;
65
+ line-height: 1.2;
66
+ white-space: nowrap;
67
+ transition: background-color .15s, border-color .15s, box-shadow .15s, transform .1s;
68
+ }
69
+ .button:focus-visible { outline: 3px solid color-mix(in srgb, var(--tone) 35%, transparent); outline-offset: 1px; }
70
+ .button:active:not(:disabled) { transform: translateY(1px); }
71
+ .button:disabled { opacity: .45; cursor: not-allowed; }
72
+ .small { padding: 6px 12px; font-size: .8125rem; }
73
+ .medium { padding: 9px 16px; font-size: .9375rem; }
74
+ .large { padding: 12px 22px; font-size: 1.0625rem; }
75
+ .full { width: 100%; }
76
+
77
+ .filled {
78
+ background: var(--tone);
79
+ color: var(--on-tone);
80
+ }
81
+ .filled:hover:not(:disabled) {
82
+ background: color-mix(in srgb, var(--tone) 88%, var(--text));
83
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--tone) 30%, transparent);
84
+ }
85
+ .outlined {
86
+ border-color: color-mix(in srgb, var(--tone) 55%, transparent);
87
+ background: none;
88
+ color: var(--tone);
89
+ }
90
+ .plain {
91
+ background: none;
92
+ color: var(--tone);
93
+ }
94
+ .outlined:hover:not(:disabled),
95
+ .plain:hover:not(:disabled) {
96
+ background: color-mix(in srgb, var(--tone) 10%, transparent);
97
+ }
package/src/Ui/Card.nn ADDED
@@ -0,0 +1,48 @@
1
+ -- 内容をまとめる面。title を書くと見出しが付く
2
+ --
3
+ -- <Card title="プロフィール">…</Card>
4
+ -- <Card padding={None}>…</Card> -- 中身を端まで広げる(リストなど)
5
+ module Ui.Card (CardProps, card) where
6
+
7
+
8
+ type CardProps =
9
+ { title :: String = ""
10
+ , padding :: Size = L
11
+ }
12
+
13
+
14
+ card :: CardProps -> [Html msg] -> Html msg
15
+ card props children =
16
+ <section class={css.card} vars={space}>
17
+ <Column gap={M}>
18
+ <Heading text={props.title} />
19
+ {children}
20
+ </Column>
21
+ </section>
22
+ where
23
+ space = { cardPadding = spacing props.padding }
24
+
25
+
26
+ heading :: { text :: String } -> Html msg
27
+ heading { text = "" } = <></>
28
+ heading { text } = <h2 class={css.title}>{text}</h2>
29
+
30
+
31
+ spacing :: Size -> String
32
+ spacing None = "0"
33
+ spacing XS = "var(--space-xs)"
34
+ spacing S = "var(--space-s)"
35
+ spacing M = "var(--space-m)"
36
+ spacing L = "var(--space-l)"
37
+ spacing XL = "var(--space-xl)"
38
+
39
+
40
+ style
41
+ .card {
42
+ padding: var(--card-padding);
43
+ border: 1px solid var(--border);
44
+ border-radius: calc(var(--radius) + 4px);
45
+ background: var(--surface);
46
+ box-shadow: var(--shadow);
47
+ }
48
+ .title { font-size: 1.0625rem; font-weight: 700; }
@@ -0,0 +1,34 @@
1
+ -- チェックボックス
2
+ --
3
+ -- <Checkbox label="利用規約に同意する" checked={model.agreed} onCheck={Agreed} />
4
+ module Ui.Checkbox (CheckboxProps, checkbox) where
5
+
6
+
7
+ type CheckboxProps msg =
8
+ { checked :: Bool
9
+ , onCheck :: Bool -> msg
10
+ , label :: String = ""
11
+ , disabled :: Bool = False
12
+ }
13
+
14
+
15
+ checkbox :: CheckboxProps msg -> Html msg
16
+ checkbox props =
17
+ <label class={css.option}>
18
+ <Row gap={S} align={Center}>
19
+ <input class={css.box} type="checkbox" checked={props.checked} disabled={props.disabled} onCheck={props.onCheck} />
20
+ <span>{props.label}</span>
21
+ </Row>
22
+ </label>
23
+
24
+
25
+ style
26
+ .option { cursor: pointer; user-select: none; }
27
+ .box {
28
+ width: 18px;
29
+ height: 18px;
30
+ margin: 0;
31
+ accent-color: var(--primary);
32
+ cursor: pointer;
33
+ }
34
+ .box:disabled, .box:disabled + span { opacity: .5; cursor: not-allowed; }
package/src/Ui/Chip.nn ADDED
@@ -0,0 +1,53 @@
1
+ -- 小さなラベル。onClick を渡すと押せるチップ(絞り込みなど)になる
2
+ --
3
+ -- <Chip tone={Success}>完了</Chip>
4
+ -- <Chip selected={isAll} onClick={Just (SetFilter All)}>すべて</Chip>
5
+ module Ui.Chip (ChipProps, chip) where
6
+
7
+ import Ui.Theme (Tone(..), toneVars)
8
+
9
+
10
+ type ChipProps msg =
11
+ { onClick :: Maybe msg = Nothing
12
+ , selected :: Bool = False
13
+ , tone :: Tone = Neutral
14
+ }
15
+
16
+
17
+ chip :: ChipProps msg -> [Html msg] -> Html msg
18
+ chip props children = <Body action={props.onClick}>{children}</Body>
19
+ where
20
+ -- 押せるチップはボタンに、押せないチップはただの文字にする
21
+ body { action = Just msg } kids = <button class={look} vars={colors} type="button" aria-pressed={pressed} onClick={msg}>{kids}</button>
22
+ body { action = Nothing } kids = <span class={look} vars={colors}>{kids}</span>
23
+
24
+ colors = toneVars props.tone
25
+ look
26
+ | props.selected = classes [css.chip, css.selected]
27
+ | otherwise = css.chip
28
+ pressed
29
+ | props.selected = "true"
30
+ | otherwise = "false"
31
+
32
+
33
+ style
34
+ .chip {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ gap: 4px;
38
+ padding: 3px 12px;
39
+ border: 1px solid color-mix(in srgb, var(--tone) 35%, transparent);
40
+ border-radius: 999px;
41
+ background: color-mix(in srgb, var(--tone) 10%, transparent);
42
+ color: var(--tone);
43
+ font-size: .8125rem;
44
+ font-weight: 600;
45
+ line-height: 1.5;
46
+ }
47
+ button.chip { transition: background-color .15s; }
48
+ button.chip:hover { background: color-mix(in srgb, var(--tone) 18%, transparent); }
49
+ .selected, button.selected:hover {
50
+ border-color: var(--tone);
51
+ background: var(--tone);
52
+ color: var(--on-tone);
53
+ }
@@ -0,0 +1,73 @@
1
+ -- 画面の上に重ねて出す対話の枠。開いているかどうかは model に持つ
2
+ --
3
+ -- <Dialog open={model.confirming} title="削除しますか?" onClose={Cancel}>
4
+ -- <p>この操作は取り消せません。</p>
5
+ -- <Row gap={S} justify={End}>
6
+ -- <Button variant={Plain} tone={Neutral} onClick={Cancel}>やめる</Button>
7
+ -- <Button tone={Danger} onClick={Confirm}>削除</Button>
8
+ -- </Row>
9
+ -- </Dialog>
10
+ --
11
+ -- 背景を押すか、右上のボタンで onClose が届く。Esc で閉じるには Keyboard.onKeyDown を購読する
12
+ module Ui.Dialog (DialogProps, dialog) where
13
+
14
+ import qualified Ui.Icon as Icon
15
+ import Ui.IconButton
16
+
17
+
18
+ type DialogProps msg =
19
+ { open :: Bool
20
+ , onClose :: msg
21
+ , title :: String = ""
22
+ }
23
+
24
+
25
+ dialog :: DialogProps msg -> [Html msg] -> Html msg
26
+ dialog props children = <Body visible={props.open} />
27
+ where
28
+ body { visible = True } =
29
+ <div class={css.overlay}>
30
+ <div class={css.backdrop} onClick={props.onClose} />
31
+ <section class={css.panel} role="dialog" aria-modal="true" aria-label={props.title}>
32
+ <Column gap={M}>
33
+ <Row gap={S} align={Center}>
34
+ <h2 class={css.title}>{props.title}</h2>
35
+ <Spacer />
36
+ <IconButton icon={Icon.close} label="閉じる" size={S} onClick={props.onClose} />
37
+ </Row>
38
+ {children}
39
+ </Column>
40
+ </section>
41
+ </div>
42
+ body { visible = False } = <></>
43
+
44
+
45
+ style
46
+ .overlay {
47
+ position: fixed;
48
+ inset: 0;
49
+ z-index: 100;
50
+ display: grid;
51
+ place-items: center;
52
+ padding: 16px;
53
+ }
54
+ .backdrop {
55
+ position: absolute;
56
+ inset: 0;
57
+ background: rgb(10 10 20 / .45);
58
+ animation: fade .15s ease-out;
59
+ }
60
+ .panel {
61
+ position: relative;
62
+ width: min(480px, 100%);
63
+ max-height: calc(100vh - 32px);
64
+ overflow: auto;
65
+ padding: 20px 24px 24px;
66
+ border-radius: calc(var(--radius) + 6px);
67
+ background: var(--surface);
68
+ box-shadow: 0 24px 64px rgb(0 0 0 / .25);
69
+ animation: rise .18s ease-out;
70
+ }
71
+ .title { font-size: 1.125rem; font-weight: 700; }
72
+ @keyframes fade { from { opacity: 0; } }
73
+ @keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }