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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # nene-lang-ui
2
2
 
3
- [Nene](https://github.com/kimuracoki/nene) の公式 UI 部品集。ボタン・入力欄・ダイアログなど、アプリを作るのに必要な部品がそろっている。ライトとダークの両方に対応し、色は CSS 変数で変えられる。
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
- - 開いているか・選んでいるかなどの状態は、ほかの値と同じく model に持つ。
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.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.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
+ }