nene-lang-ui 0.1.1 → 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/README.md +7 -2
- package/package.json +2 -2
- package/src/Ui/Accordion.nn +43 -0
- package/src/Ui/Menu.nn +122 -0
- package/src/Ui/Popover.nn +72 -0
- package/src/Ui/Tooltip.nn +45 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nene-lang-ui
|
|
2
2
|
|
|
3
|
-
[Nene](https://github.com/kimuracoki/nene) の公式 UI
|
|
3
|
+
[Nene](https://github.com/kimuracoki/nene) の公式 UI 部品集。ボタン・入力欄・ダイアログ・メニューなど 20 の部品がそろっている。ライトとダークの両方に対応し、色は CSS 変数で変えられる。
|
|
4
4
|
|
|
5
5
|
`nene new` で作ったプロジェクトには、はじめから入っている。あとから入れるときは:
|
|
6
6
|
|
|
@@ -39,10 +39,15 @@ import Ui.Theme (Tone(..))
|
|
|
39
39
|
| `Ui.Spinner` | `<Spinner />` | `size`、`label` |
|
|
40
40
|
| `Ui.Avatar` | `<Avatar />` | `name`(頭文字を出す)、`src`、`size` |
|
|
41
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` |
|
|
42
46
|
|
|
43
47
|
- 色の種類 `tone` は、どの部品でも `Ui.Theme` の `Primary` `Neutral` `Success` `Warning` `Danger`。
|
|
44
48
|
- 大きさ `size` は、レイアウトタグと同じ `S` `M` `L`。
|
|
45
|
-
-
|
|
49
|
+
- アプリが知る必要のある状態(入力した値、選んだタブ、Dialog を開いているか)は model に持つ。
|
|
50
|
+
- Menu・Popover・Accordion・Tooltip の開け閉めはブラウザが受け持つので、model に何も足さなくてよい。Menu と Popover の `id` は、ページの中で重ならない名前にする。
|
|
46
51
|
|
|
47
52
|
## 色と形を変える
|
|
48
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nene-lang-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Nene の公式 UI 部品集。ボタン・入力欄・ダイアログなど",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"nene": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"nene-lang": "^0.1.
|
|
14
|
+
"nene-lang": "^0.1.2"
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
@@ -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); }
|
package/src/Ui/Menu.nn
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
-- ボタンを押すと開くメニュー。開け閉めはブラウザが受け持つので、model に何も足さなくてよい
|
|
2
|
+
-- (外を押すか Esc で閉じ、項目を押すとメッセージが届いて閉じる)
|
|
3
|
+
--
|
|
4
|
+
-- <Menu id="sort" label="並び替え" items={sortItems} />
|
|
5
|
+
-- where
|
|
6
|
+
-- sortItems =
|
|
7
|
+
-- [ Item { label = "新しい順", onClick = SortBy Newest }
|
|
8
|
+
-- , Item { label = "削除", onClick = Remove, icon = Just Icon.delete, tone = Danger }
|
|
9
|
+
-- ]
|
|
10
|
+
--
|
|
11
|
+
-- id は、ページの中でほかと重ならない名前にする
|
|
12
|
+
module Ui.Menu (MenuProps, Item, menu) where
|
|
13
|
+
|
|
14
|
+
import Ui.Button (Variant(..))
|
|
15
|
+
import qualified Ui.Icon as Icon
|
|
16
|
+
import Ui.Theme (Tone(..), toneVars)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
type Item msg =
|
|
20
|
+
{ label :: String
|
|
21
|
+
, onClick :: msg
|
|
22
|
+
, icon :: Maybe Icon.Icon = Nothing
|
|
23
|
+
, tone :: Tone = Neutral
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type MenuProps msg =
|
|
27
|
+
{ id :: String
|
|
28
|
+
, label :: String
|
|
29
|
+
, items :: [Item msg]
|
|
30
|
+
, variant :: Variant = Outlined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
menu :: MenuProps msg -> Html msg
|
|
35
|
+
menu props =
|
|
36
|
+
<div class={css.menu} vars={anchor}>
|
|
37
|
+
<button class={trigger} type="button" popovertarget={props.id} aria-haspopup="menu">
|
|
38
|
+
<Row gap={S} align={Center}>
|
|
39
|
+
<span>{props.label}</span>
|
|
40
|
+
<Icon name={Icon.chevronDown} />
|
|
41
|
+
</Row>
|
|
42
|
+
</button>
|
|
43
|
+
<div class={css.list} id={props.id} popover role="menu">
|
|
44
|
+
<Column>{map entry props.items}</Column>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
where
|
|
48
|
+
anchor = { anchor = "--menu-" ++ props.id }
|
|
49
|
+
trigger = classes [css.trigger, variantClass props.variant]
|
|
50
|
+
|
|
51
|
+
-- 項目を押すと、メッセージが届き、メニューが閉じる
|
|
52
|
+
entry item =
|
|
53
|
+
<button class={css.item} vars={colors} type="button" role="menuitem" popovertarget={props.id} popovertargetaction="hide" onClick={item.onClick}>
|
|
54
|
+
<Row gap={S} align={Center}>
|
|
55
|
+
<Mark icon={item.icon} />
|
|
56
|
+
<span>{item.label}</span>
|
|
57
|
+
</Row>
|
|
58
|
+
</button>
|
|
59
|
+
where
|
|
60
|
+
colors = toneVars item.tone
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
mark :: { icon :: Maybe Icon.Icon } -> Html msg
|
|
64
|
+
mark { icon = Just i } = <Icon name={i} />
|
|
65
|
+
mark { icon = Nothing } = <></>
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
variantClass :: Variant -> Class
|
|
69
|
+
variantClass Filled = css.filled
|
|
70
|
+
variantClass Outlined = css.outlined
|
|
71
|
+
variantClass Plain = css.plain
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
style
|
|
75
|
+
.menu { display: inline-block; anchor-name: var(--anchor); }
|
|
76
|
+
.trigger {
|
|
77
|
+
padding: 8px 12px;
|
|
78
|
+
border: 1px solid transparent;
|
|
79
|
+
border-radius: var(--radius);
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
line-height: 1.2;
|
|
82
|
+
}
|
|
83
|
+
.filled { background: var(--primary); color: var(--on-tone); }
|
|
84
|
+
.outlined { border-color: var(--border); background: var(--surface); }
|
|
85
|
+
.plain { background: none; }
|
|
86
|
+
.outlined:hover, .plain:hover { background: color-mix(in srgb, var(--text) 6%, var(--surface)); }
|
|
87
|
+
.trigger:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 35%, transparent); }
|
|
88
|
+
|
|
89
|
+
.list {
|
|
90
|
+
min-width: 180px;
|
|
91
|
+
margin: 0;
|
|
92
|
+
padding: 4px;
|
|
93
|
+
border: 1px solid var(--border);
|
|
94
|
+
border-radius: var(--radius);
|
|
95
|
+
background: var(--surface);
|
|
96
|
+
color: var(--text);
|
|
97
|
+
box-shadow: 0 12px 32px rgb(0 0 0 / .14);
|
|
98
|
+
}
|
|
99
|
+
/* ボタンのすぐ下に出す。この機能のないブラウザでは、画面の中央に出る */
|
|
100
|
+
@supports (anchor-name: --a) {
|
|
101
|
+
.list {
|
|
102
|
+
position-anchor: var(--anchor);
|
|
103
|
+
inset: auto;
|
|
104
|
+
top: anchor(bottom);
|
|
105
|
+
left: anchor(left);
|
|
106
|
+
margin-top: 4px;
|
|
107
|
+
position-try-fallbacks: flip-block, flip-inline;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
.item {
|
|
111
|
+
width: 100%;
|
|
112
|
+
padding: 8px 10px;
|
|
113
|
+
border: none;
|
|
114
|
+
border-radius: calc(var(--radius) - 4px);
|
|
115
|
+
background: none;
|
|
116
|
+
color: var(--tone);
|
|
117
|
+
text-align: left;
|
|
118
|
+
}
|
|
119
|
+
.item:hover, .item:focus-visible {
|
|
120
|
+
outline: none;
|
|
121
|
+
background: color-mix(in srgb, var(--tone) 10%, transparent);
|
|
122
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- ボタンを押すと開く、自由な中身の吹き出し。外を押すか Esc で閉じる。model に何も足さなくてよい
|
|
2
|
+
--
|
|
3
|
+
-- <Popover id="profile" label="プロフィール">
|
|
4
|
+
-- <Column gap={S}>…</Column>
|
|
5
|
+
-- </Popover>
|
|
6
|
+
--
|
|
7
|
+
-- id は、ページの中でほかと重ならない名前にする
|
|
8
|
+
module Ui.Popover (PopoverProps, popover) where
|
|
9
|
+
|
|
10
|
+
import Ui.Button (Variant(..))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
type PopoverProps =
|
|
14
|
+
{ id :: String
|
|
15
|
+
, label :: String
|
|
16
|
+
, variant :: Variant = Outlined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
popover :: PopoverProps -> [Html msg] -> Html msg
|
|
21
|
+
popover props children =
|
|
22
|
+
<div class={css.popover} vars={anchor}>
|
|
23
|
+
<button class={trigger} type="button" popovertarget={props.id}>{props.label}</button>
|
|
24
|
+
<div class={css.panel} id={props.id} popover>{children}</div>
|
|
25
|
+
</div>
|
|
26
|
+
where
|
|
27
|
+
anchor = { anchor = "--popover-" ++ props.id }
|
|
28
|
+
trigger = classes [css.trigger, variantClass props.variant]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
variantClass :: Variant -> Class
|
|
32
|
+
variantClass Filled = css.filled
|
|
33
|
+
variantClass Outlined = css.outlined
|
|
34
|
+
variantClass Plain = css.plain
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
style
|
|
38
|
+
.popover { display: inline-block; anchor-name: var(--anchor); }
|
|
39
|
+
.trigger {
|
|
40
|
+
padding: 8px 12px;
|
|
41
|
+
border: 1px solid transparent;
|
|
42
|
+
border-radius: var(--radius);
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
line-height: 1.2;
|
|
45
|
+
}
|
|
46
|
+
.filled { background: var(--primary); color: var(--on-tone); }
|
|
47
|
+
.outlined { border-color: var(--border); background: var(--surface); }
|
|
48
|
+
.plain { background: none; }
|
|
49
|
+
.outlined:hover, .plain:hover { background: color-mix(in srgb, var(--text) 6%, var(--surface)); }
|
|
50
|
+
.trigger:focus-visible { outline: 3px solid color-mix(in srgb, var(--primary) 35%, transparent); }
|
|
51
|
+
.panel {
|
|
52
|
+
min-width: 220px;
|
|
53
|
+
max-width: min(360px, calc(100vw - 32px));
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 16px;
|
|
56
|
+
border: 1px solid var(--border);
|
|
57
|
+
border-radius: calc(var(--radius) + 2px);
|
|
58
|
+
background: var(--surface);
|
|
59
|
+
color: var(--text);
|
|
60
|
+
box-shadow: 0 12px 32px rgb(0 0 0 / .14);
|
|
61
|
+
}
|
|
62
|
+
/* ボタンのすぐ下に出す。この機能のないブラウザでは、画面の中央に出る */
|
|
63
|
+
@supports (anchor-name: --a) {
|
|
64
|
+
.panel {
|
|
65
|
+
position-anchor: var(--anchor);
|
|
66
|
+
inset: auto;
|
|
67
|
+
top: anchor(bottom);
|
|
68
|
+
left: anchor(left);
|
|
69
|
+
margin-top: 6px;
|
|
70
|
+
position-try-fallbacks: flip-block, flip-inline;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- 子要素にマウスを載せるか、キーボードで選んだときに出る説明
|
|
2
|
+
--
|
|
3
|
+
-- <Tooltip text="保存(⌘S)">
|
|
4
|
+
-- <IconButton icon={Icon.check} label="保存" onClick={Save} />
|
|
5
|
+
-- </Tooltip>
|
|
6
|
+
module Ui.Tooltip (TooltipProps, tooltip) where
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
type TooltipProps =
|
|
10
|
+
{ text :: String
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
tooltip :: TooltipProps -> [Html msg] -> Html msg
|
|
15
|
+
tooltip props children =
|
|
16
|
+
<span class={css.anchor}>
|
|
17
|
+
{children}
|
|
18
|
+
<span class={css.tip} role="tooltip">{props.text}</span>
|
|
19
|
+
</span>
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
style
|
|
23
|
+
.anchor { position: relative; display: inline-flex; }
|
|
24
|
+
.tip {
|
|
25
|
+
position: absolute;
|
|
26
|
+
bottom: calc(100% + 6px);
|
|
27
|
+
left: 50%;
|
|
28
|
+
z-index: 50;
|
|
29
|
+
padding: 4px 8px;
|
|
30
|
+
border-radius: 6px;
|
|
31
|
+
background: var(--text);
|
|
32
|
+
color: var(--surface);
|
|
33
|
+
font-size: .75rem;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
opacity: 0;
|
|
37
|
+
transform: translate(-50%, 4px);
|
|
38
|
+
transition: opacity .12s, transform .12s;
|
|
39
|
+
}
|
|
40
|
+
.anchor:hover > .tip,
|
|
41
|
+
.anchor:focus-within > .tip {
|
|
42
|
+
opacity: 1;
|
|
43
|
+
transform: translate(-50%, 0);
|
|
44
|
+
transition-delay: .3s;
|
|
45
|
+
}
|