@mezzanine-stack/ui 0.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/Accordion.astro +13 -0
- package/Alert.astro +17 -0
- package/Breadcrumbs.astro +27 -0
- package/Button.astro +13 -0
- package/Card.astro +11 -0
- package/Checkbox.astro +11 -0
- package/Container.astro +16 -0
- package/EmptyState.astro +12 -0
- package/Field.astro +23 -0
- package/Footer.astro +20 -0
- package/Header.astro +39 -0
- package/Heading.astro +21 -0
- package/Input.astro +17 -0
- package/Modal.astro +37 -0
- package/Pagination.astro +49 -0
- package/README.md +31 -0
- package/Radio.astro +11 -0
- package/Section.astro +16 -0
- package/Select.astro +18 -0
- package/Table.astro +34 -0
- package/Tabs.astro +101 -0
- package/Tag.astro +13 -0
- package/Text.astro +12 -0
- package/Textarea.astro +31 -0
- package/Toaster.astro +85 -0
- package/Tooltip.astro +11 -0
- package/package.json +46 -0
package/Accordion.astro
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Accordion
|
|
4
|
+
* props: { summary: string, open?: boolean, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { summary, open = false, klass = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<details class={`mz-accordion ${klass}`.trim()} {...(open ? {open:true} : {})}>
|
|
9
|
+
<summary class="cluster" style="justify-content:space-between">
|
|
10
|
+
<span>{summary}</span><span aria-hidden="true">▾</span>
|
|
11
|
+
</summary>
|
|
12
|
+
<div class="stack"><slot /></div>
|
|
13
|
+
</details>
|
package/Alert.astro
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Alert: 情報/成功/警告/危険
|
|
4
|
+
* props: { variant?: 'info'|'success'|'warning'|'danger', title?: string, role?: 'status'|'alert', class?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { variant = 'info', title, role = (variant === 'danger' ? 'alert' : 'status'), class: klass = '' } = Astro.props;
|
|
7
|
+
const tone = {
|
|
8
|
+
info: { bg: 'rgba(59,130,246,.10)', border: 'color-mix(in oklab, var(--color-primary) 45%, white)', fg: 'var(--color-fg)' },
|
|
9
|
+
success:{ bg: 'rgba(22,163,74,.10)', border: 'rgba(22,163,74,.35)', fg: 'var(--color-fg)' },
|
|
10
|
+
warning:{ bg: 'rgba(245,158,11,.12)', border: 'rgba(245,158,11,.35)', fg: 'var(--color-fg)' },
|
|
11
|
+
danger: { bg: 'rgba(239,68,68,.12)', border: 'rgba(239,68,68,.35)', fg: 'var(--color-fg)' },
|
|
12
|
+
}[variant];
|
|
13
|
+
---
|
|
14
|
+
<div role={role} class={`mz-card ${klass}`} style={`background:${tone.bg};border-color:${tone.border};`}>
|
|
15
|
+
{title && <strong style="display:block;margin-bottom:8px">{title}</strong>}
|
|
16
|
+
<div style={`color:${tone.fg}`}><slot /></div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Breadcrumbs
|
|
4
|
+
* props:
|
|
5
|
+
* - items: Array<{ label: string; href?: string }>
|
|
6
|
+
* ※ 最後の要素が現ページ。href 省略 or undefined なら aria-current="page"
|
|
7
|
+
* - klass?: string
|
|
8
|
+
*/
|
|
9
|
+
const { items = [], klass = "" } = Astro.props;
|
|
10
|
+
const lastIndex = Math.max(0, items.length - 1);
|
|
11
|
+
---
|
|
12
|
+
<nav aria-label="breadcrumb" class={`mz-breadcrumb ${klass}`.trim()}>
|
|
13
|
+
<ol>
|
|
14
|
+
{items.map((it: { href: string | URL | null | undefined; label: unknown; }, i: number) => {
|
|
15
|
+
const isCurrent = i === lastIndex || !it.href;
|
|
16
|
+
return (
|
|
17
|
+
<li>
|
|
18
|
+
{isCurrent ? (
|
|
19
|
+
<span aria-current="page">{it.label}</span>
|
|
20
|
+
) : (
|
|
21
|
+
<a href={it.href}>{it.label}</a>
|
|
22
|
+
)}
|
|
23
|
+
</li>
|
|
24
|
+
);
|
|
25
|
+
})}
|
|
26
|
+
</ol>
|
|
27
|
+
</nav>
|
package/Button.astro
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
const { href, variant = "default", ariaLabel, rel, target } = Astro.props;
|
|
3
|
+
|
|
4
|
+
const cls = `mz-button ${variant === "primary" ? "primary" : ""}`;
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
{href ? (
|
|
8
|
+
<a class={cls} href={href} aria-label={ariaLabel} rel={rel} target={target}><slot /></a>
|
|
9
|
+
) :
|
|
10
|
+
(
|
|
11
|
+
<button class={cls} type="button" aria-label={ariaLabel}><slot /></button>
|
|
12
|
+
)
|
|
13
|
+
}
|
package/Card.astro
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Card: 汎用カードラッパ
|
|
4
|
+
* props: { as?: 'article'|'section'|'div', class?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { as = 'article', class: klass = '' } = Astro.props;
|
|
7
|
+
const cls = `mz-card stack ${klass}`.trim();
|
|
8
|
+
---
|
|
9
|
+
{as === 'section' && (<section class={cls}><slot /></section>)}
|
|
10
|
+
{as === 'div' && (<div class={cls}><slot /></div>)}
|
|
11
|
+
{!['section','div'].includes(as) && (<article class={cls}><slot /></article>)}
|
package/Checkbox.astro
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Checkbox.astro
|
|
3
|
+
/**
|
|
4
|
+
* props: { id: string, label?: string, checked?: boolean, required?: boolean, describedBy?: string, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { id, label, checked = false, required = false, describedBy, klass = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<label class={`mz-check ${klass}`.trim()}>
|
|
9
|
+
<input id={id} type="checkbox" checked={checked} aria-describedby={describedBy} required={required} />
|
|
10
|
+
<span>{label}</span>
|
|
11
|
+
</label>
|
package/Container.astro
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Container: 幅制御コンポーネント
|
|
4
|
+
* @param _props : { as?: "div" | "section" | "article" | "main" | "aside" | "header" | "footer" }
|
|
5
|
+
* @returns {JSX.Element}
|
|
6
|
+
*/
|
|
7
|
+
const { as = "div", class: klass = '' } = Astro.props;
|
|
8
|
+
const classes = `mz-container ${klass}`.trim();
|
|
9
|
+
---
|
|
10
|
+
{as === 'section' && (<section class={classes}><slot /></section>)}
|
|
11
|
+
{as === 'article' && (<article class={classes}><slot /></article>)}
|
|
12
|
+
{as === 'main' && (<main class={classes}><slot /></main>)}
|
|
13
|
+
{as === 'aside' && (<aside class={classes}><slot /></aside>)}
|
|
14
|
+
{as === 'header' && (<header class={classes}><slot /></header>)}
|
|
15
|
+
{as === 'footer' && (<footer class={classes}><slot /></footer>)}
|
|
16
|
+
{as === 'div' && (<div class={classes}><slot /></div>)}
|
package/EmptyState.astro
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* EmptyState
|
|
4
|
+
* props: { title?: string, message?: string, ctaHref?: string, ctaLabel?: string, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { title = "コンテンツがありません", message = "はじめての項目を作成しましょう。", ctaHref, ctaLabel = "作成する", klass = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<section class={`mz-card stack ${klass}`.trim()} style="text-align:center; padding: var(--space-8);">
|
|
9
|
+
<h3 style="margin:0">{title}</h3>
|
|
10
|
+
<p style="opacity:.8">{message}</p>
|
|
11
|
+
{ctaHref && <a class="mz-button primary" href={ctaHref}>{ctaLabel}</a>}
|
|
12
|
+
</section>
|
package/Field.astro
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Field: ラベル/説明/エラーをまとめるラッパ
|
|
4
|
+
* props: { id: string, label?: string, help?: string, error?: string, required?: boolean, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { id, label, help, error, required = false, klass = "" } = Astro.props;
|
|
7
|
+
const helpId = help ? `${id}-help` : undefined;
|
|
8
|
+
const errorId = error ? `${id}-error` : undefined;
|
|
9
|
+
---
|
|
10
|
+
<div class={`mz-field stack ${klass}`.trim()}>
|
|
11
|
+
{label && (
|
|
12
|
+
<label for={id} class="mz-label">
|
|
13
|
+
{label}{required && <span aria-hidden="true" class="req">*</span>}
|
|
14
|
+
</label>
|
|
15
|
+
)}
|
|
16
|
+
|
|
17
|
+
<div class="mz-control">
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{help && <p id={helpId} class="mz-help">{help}</p>}
|
|
22
|
+
{error && <p id={errorId} class="mz-error" role="alert">{error}</p>}
|
|
23
|
+
</div>
|
package/Footer.astro
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Footer
|
|
4
|
+
* props: { links?: Array<{label:string; href:string}>, note?: string, year?: number }
|
|
5
|
+
*/
|
|
6
|
+
const { links = [], note = "", year = new Date().getFullYear() } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<footer class="mz-footer">
|
|
9
|
+
<div class="container cluster" style="justify-content:space-between; align-items:center;">
|
|
10
|
+
<small>© {year} Mezzanine</small>
|
|
11
|
+
{note && <small style="opacity:.8">{note}</small>}
|
|
12
|
+
{links.length > 0 && (
|
|
13
|
+
<nav aria-label="Footer">
|
|
14
|
+
<ul class="cluster">
|
|
15
|
+
{links.map(l => <li><a href={l.href}>{l.label}</a></li>)}
|
|
16
|
+
</ul>
|
|
17
|
+
</nav>
|
|
18
|
+
)}
|
|
19
|
+
</div>
|
|
20
|
+
</footer>
|
package/Header.astro
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Header
|
|
4
|
+
* props:
|
|
5
|
+
* - title: string サイトタイトル
|
|
6
|
+
* - href?: string タイトルリンク先 (default="/")
|
|
7
|
+
* - links?: Array<{ label: string; href: string }>
|
|
8
|
+
*/
|
|
9
|
+
const { title = "Mezzanine", href = "/", links = [] } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
<header class="mz-header">
|
|
12
|
+
<div class="mz-container cluster" style="justify-content:space-between; align-items:center;">
|
|
13
|
+
<!-- ブランド -->
|
|
14
|
+
<a href={href} class="mz-brand">{title}</a>
|
|
15
|
+
|
|
16
|
+
<!-- PCナビ -->
|
|
17
|
+
{links.length > 0 && (
|
|
18
|
+
<nav class="mz-nav" aria-label="Main navigation">
|
|
19
|
+
<ul class="cluster">
|
|
20
|
+
{links.map(link => (
|
|
21
|
+
<li><a href={link.href}>{link.label}</a></li>
|
|
22
|
+
))}
|
|
23
|
+
</ul>
|
|
24
|
+
</nav>
|
|
25
|
+
)}
|
|
26
|
+
|
|
27
|
+
<!-- モバイルナビ -->
|
|
28
|
+
{links.length > 0 && (
|
|
29
|
+
<details class="mz-nav-mobile">
|
|
30
|
+
<summary aria-label="メニューを開く">☰</summary>
|
|
31
|
+
<ul class="stack">
|
|
32
|
+
{links.map(link => (
|
|
33
|
+
<li><a href={link.href}>{link.label}</a></li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
</details>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
</header>
|
package/Heading.astro
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Heading: レベルとサイズを分離
|
|
4
|
+
* props: { level?: 1|2|3|4|5|6, size?: 'xs'|'sm'|'md'|'lg'|'xl', class?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { level = 2, size = 'lg', class: klass = '' } = Astro.props;
|
|
7
|
+
const sizeMap = {
|
|
8
|
+
xs: 'var(--text-sm)',
|
|
9
|
+
sm: 'var(--text-md)',
|
|
10
|
+
md: 'var(--text-lg)',
|
|
11
|
+
lg: 'var(--text-xl)',
|
|
12
|
+
xl: 'clamp(24px, 1.6rem, 28px)'
|
|
13
|
+
};
|
|
14
|
+
const style = `margin:0;font-size:${sizeMap[size] ?? sizeMap.lg}`;
|
|
15
|
+
---
|
|
16
|
+
{level === 1 && (<h1 style={style} class={klass}><slot /></h1>)}
|
|
17
|
+
{level === 2 && (<h2 style={style} class={klass}><slot /></h2>)}
|
|
18
|
+
{level === 3 && (<h3 style={style} class={klass}><slot /></h3>)}
|
|
19
|
+
{level === 4 && (<h4 style={style} class={klass}><slot /></h4>)}
|
|
20
|
+
{level === 5 && (<h5 style={style} class={klass}><slot /></h5>)}
|
|
21
|
+
{level === 6 && (<h6 style={style} class={klass}><slot /></h6>)}
|
package/Input.astro
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Input: 単一行テキスト
|
|
4
|
+
* props: { id: string, type?: string, value?: string, placeholder?: string, invalid?: boolean, required?: boolean, describedBy?: string, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { id, type = "text", value, placeholder, invalid = false, required = false, describedBy, klass = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<input
|
|
9
|
+
id={id}
|
|
10
|
+
class={`mz-input ${klass}`.trim()}
|
|
11
|
+
type={type}
|
|
12
|
+
value={value}
|
|
13
|
+
placeholder={placeholder}
|
|
14
|
+
aria-invalid={invalid ? "true" : "false"}
|
|
15
|
+
aria-describedby={describedBy}
|
|
16
|
+
required={required}
|
|
17
|
+
/>
|
package/Modal.astro
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Modal (dialog)
|
|
4
|
+
* props: { id: string, title?: string, interactive?: boolean, klass?: string }
|
|
5
|
+
* 使い方:
|
|
6
|
+
* <Modal id="m1" title="設定" interactive>
|
|
7
|
+
* <p>本文</p>
|
|
8
|
+
* </Modal>
|
|
9
|
+
* <a href="#m1" data-open="m1">開く</a>
|
|
10
|
+
*/
|
|
11
|
+
const { id, title, interactive = false, klass = "" } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
<dialog id={id} class={`mz-modal ${klass}`.trim()} aria-labelledby={title ? `${id}-title` : undefined}>
|
|
14
|
+
<form method="dialog" class="stack">
|
|
15
|
+
{title && <h3 id={`${id}-title`} style="margin:0">{title}</h3>}
|
|
16
|
+
<slot />
|
|
17
|
+
<div class="cluster" style="justify-content:flex-end">
|
|
18
|
+
<button class="mz-button" value="cancel">閉じる</button>
|
|
19
|
+
</div>
|
|
20
|
+
</form>
|
|
21
|
+
</dialog>
|
|
22
|
+
|
|
23
|
+
{interactive && (
|
|
24
|
+
<script is:inline>
|
|
25
|
+
(() => {
|
|
26
|
+
const dlg = document.getElementById({id: JSON.stringify(id)});
|
|
27
|
+
if (!dlg) return;
|
|
28
|
+
document.addEventListener('click', (e) => {
|
|
29
|
+
const openBtn = e.target.closest('[data-open="{id}"]');
|
|
30
|
+
if (openBtn) { e.preventDefault(); dlg.showModal(); }
|
|
31
|
+
});
|
|
32
|
+
dlg.addEventListener('click', (e) => {
|
|
33
|
+
if (e.target === dlg) dlg.close(); // backdrop click
|
|
34
|
+
});
|
|
35
|
+
})();
|
|
36
|
+
</script>
|
|
37
|
+
)}
|
package/Pagination.astro
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Pagination(ページ番号内蔵)
|
|
4
|
+
* props:
|
|
5
|
+
* - pages: { url: string; pageNumber: number }[]
|
|
6
|
+
* - currentPage: number (現在のページ番号, 1始まり)
|
|
7
|
+
* - prevLabel?: string
|
|
8
|
+
* - nextLabel?: string
|
|
9
|
+
* - klass?: string
|
|
10
|
+
*/
|
|
11
|
+
const {
|
|
12
|
+
pages = [],
|
|
13
|
+
currentPage = 1,
|
|
14
|
+
prevLabel = "Prev",
|
|
15
|
+
nextLabel = "Next",
|
|
16
|
+
klass = ""
|
|
17
|
+
} = Astro.props;
|
|
18
|
+
|
|
19
|
+
const currentIndex = pages.findIndex((p) => p.pageNumber === currentPage);
|
|
20
|
+
const prev = currentIndex > 0 ? pages[currentIndex - 1] : undefined;
|
|
21
|
+
const next = currentIndex >= 0 && currentIndex < pages.length - 1 ? pages[currentIndex + 1] : undefined;
|
|
22
|
+
---
|
|
23
|
+
<nav aria-label="pagination" class={`mz-pagination ${klass}`.trim()}>
|
|
24
|
+
<div class="container cluster" style="justify-content:space-between">
|
|
25
|
+
<div>
|
|
26
|
+
{prev ? (
|
|
27
|
+
<a class="mz-button" href={prev.url} rel="prev">← {prevLabel}</a>
|
|
28
|
+
) : (
|
|
29
|
+
<span />
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
<div class="cluster">
|
|
33
|
+
{pages.map((p) =>
|
|
34
|
+
p.pageNumber === currentPage ? (
|
|
35
|
+
<span class="mz-button" aria-current="page">{p.pageNumber}</span>
|
|
36
|
+
) : (
|
|
37
|
+
<a href={p.url} class="mz-button">{p.pageNumber}</a>
|
|
38
|
+
)
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
<div>
|
|
42
|
+
{next ? (
|
|
43
|
+
<a class="mz-button" href={next.url} rel="next">{nextLabel} →</a>
|
|
44
|
+
) : (
|
|
45
|
+
<span />
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</nav>
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @mezzanine-stack/ui
|
|
2
|
+
|
|
3
|
+
Astro ベースの共通 UI コンポーネント群です。見た目の基盤は `@mezzanine-stack/tokens` と `@mezzanine-stack/css` に依存します。
|
|
4
|
+
|
|
5
|
+
## 設計方針
|
|
6
|
+
|
|
7
|
+
- Astro コンポーネントを package exports で直接公開
|
|
8
|
+
- デザイントークンと共通 CSS を前提に、アプリ側で組み合わせて利用
|
|
9
|
+
- `astro` は `peerDependencies` としてホストアプリ側で解決
|
|
10
|
+
|
|
11
|
+
## 主なコンポーネント
|
|
12
|
+
|
|
13
|
+
- レイアウト: `Container`, `Section`, `Header`, `Footer`
|
|
14
|
+
- フォーム: `Field`, `Input`, `Textarea`, `Select`, `Checkbox`, `Radio`
|
|
15
|
+
- ナビゲーション: `Breadcrumbs`, `Pagination`, `Tabs`
|
|
16
|
+
- フィードバック: `Alert`, `EmptyState`, `Toaster`, `Modal`, `Tooltip`
|
|
17
|
+
- 表示: `Heading`, `Text`, `Tag`, `Card`, `Table`, `Accordion`, `Button`
|
|
18
|
+
|
|
19
|
+
## 利用例
|
|
20
|
+
|
|
21
|
+
```astro
|
|
22
|
+
---
|
|
23
|
+
import Container from "@mezzanine-stack/ui/Container.astro";
|
|
24
|
+
import Heading from "@mezzanine-stack/ui/Heading.astro";
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<Container>
|
|
28
|
+
<Heading level={2}>Hello</Heading>
|
|
29
|
+
</Container>
|
|
30
|
+
```
|
|
31
|
+
|
package/Radio.astro
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Radio.astro
|
|
3
|
+
/**
|
|
4
|
+
* props: { id: string, name: string, label?: string, checked?: boolean, required?: boolean, describedBy?: string, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { id, name, label, checked = false, required = false, describedBy, klass = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<label class={`mz-radio ${klass}`.trim()}>
|
|
9
|
+
<input id={id} name={name} type="radio" checked={checked} aria-describedby={describedBy} required={required} />
|
|
10
|
+
<span>{label}</span>
|
|
11
|
+
</label>
|
package/Section.astro
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Section: セクション余白+見出し(任意)
|
|
4
|
+
* props: { title?: string, lead?: string, id?: string, padded?: boolean, class?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { title, lead, id, padded = true, class: klass = '' } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<section id={id} class={`mz-stack ${padded ? '' : ''} ${klass}`.trim()} style={padded ? undefined : 'margin-top:0'}>
|
|
9
|
+
{title && (
|
|
10
|
+
<header class="mz-stack">
|
|
11
|
+
<h2 style="margin:0;font-size:var(--text-xl)">{title}</h2>
|
|
12
|
+
{lead && <p style="opacity:.8">{lead}</p>}
|
|
13
|
+
</header>
|
|
14
|
+
)}
|
|
15
|
+
<slot />
|
|
16
|
+
</section>
|
package/Select.astro
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Select
|
|
4
|
+
* props: { id: string, options: Array<{label:string; value:string}>, value?: string, placeholder?: string, invalid?: boolean, required?: boolean, describedBy?: string, klass?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { id, options = [], value, placeholder, invalid = false, required = false, describedBy, klass = "" } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
<select
|
|
9
|
+
id={id}
|
|
10
|
+
class={`mz-select ${klass}`.trim()}
|
|
11
|
+
value={value}
|
|
12
|
+
aria-invalid={invalid ? "true" : "false"}
|
|
13
|
+
aria-describedby={describedBy}
|
|
14
|
+
required={required}
|
|
15
|
+
>
|
|
16
|
+
{placeholder && <option value="" disabled selected={!value}>{placeholder}</option>}
|
|
17
|
+
{options.map(opt => <option value={opt.value} selected={value === opt.value}>{opt.label}</option>)}
|
|
18
|
+
</select>
|
package/Table.astro
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Table
|
|
4
|
+
* props: {
|
|
5
|
+
* caption?: string;
|
|
6
|
+
* headers?: string[]; // thead のテキスト(簡易)
|
|
7
|
+
* dense?: boolean; // 余白を詰める
|
|
8
|
+
* zebra?: boolean; // 交互色
|
|
9
|
+
* klass?: string;
|
|
10
|
+
* }
|
|
11
|
+
* 使い方:
|
|
12
|
+
* <Table headers={["Title","Date","Tags"]}>
|
|
13
|
+
* <Fragment slot="body">
|
|
14
|
+
* <tr><td>Post A</td><td>2025/09/01</td><td>news</td></tr>
|
|
15
|
+
* </Fragment>
|
|
16
|
+
* </Table>
|
|
17
|
+
*/
|
|
18
|
+
const { caption, headers = [], dense = false, zebra = false, klass = "" } = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
<div class={`table-scroll ${klass}`.trim()}>
|
|
21
|
+
<table class={`mz-table ${dense ? "dense" : ""} ${zebra ? "zebra" : ""}`.trim()}>
|
|
22
|
+
{caption && <caption>{caption}</caption>}
|
|
23
|
+
{headers.length > 0 && (
|
|
24
|
+
<thead>
|
|
25
|
+
<tr>
|
|
26
|
+
{headers.map(h => <th scope="col">{h}</th>)}
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
)}
|
|
30
|
+
<tbody>
|
|
31
|
+
<slot name="body" />
|
|
32
|
+
</tbody>
|
|
33
|
+
</table>
|
|
34
|
+
</div>
|
package/Tabs.astro
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Tabs (progressive enhanced)
|
|
4
|
+
* props:
|
|
5
|
+
* - tabs: Array<{ id: string; label: string }>
|
|
6
|
+
* - interactive?: boolean // デフォ false。true で薄JSを有効化
|
|
7
|
+
*/
|
|
8
|
+
const { tabs = [], interactive = false } = Astro.props;
|
|
9
|
+
const firstId = tabs[0]?.id;
|
|
10
|
+
---
|
|
11
|
+
<div class="mz-tabs" data-tabs {...(interactive ? {'data-interactive': 'true'} : {})}>
|
|
12
|
+
<nav class="mz-tablist" role="tablist" aria-label="Tabs">
|
|
13
|
+
<ul class="cluster">
|
|
14
|
+
{tabs.map((t, i) => (
|
|
15
|
+
<li>
|
|
16
|
+
<a
|
|
17
|
+
id={`${t.id}-tab`}
|
|
18
|
+
role="tab"
|
|
19
|
+
href={`#${t.id}`}
|
|
20
|
+
aria-controls={t.id}
|
|
21
|
+
aria-selected={i === 0 ? "true" : "false"}
|
|
22
|
+
tabindex={i === 0 ? "0" : "-1"}
|
|
23
|
+
>{t.label}</a>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
</nav>
|
|
28
|
+
|
|
29
|
+
<div class="mz-tabpanels">
|
|
30
|
+
<slot />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{interactive && (
|
|
35
|
+
<script is:inline>
|
|
36
|
+
// 極小強化スクリプト(約1KB)
|
|
37
|
+
(() => {
|
|
38
|
+
const root = document.currentScript?.previousElementSibling;
|
|
39
|
+
if (!root || !root.matches('[data-tabs][data-interactive]')) return;
|
|
40
|
+
|
|
41
|
+
const tabs = Array.from(root.querySelectorAll('[role="tab"]'));
|
|
42
|
+
const panels = Array.from(root.querySelectorAll('.mz-tabpanels > *'));
|
|
43
|
+
|
|
44
|
+
const byHash = () => {
|
|
45
|
+
const id = location.hash?.slice(1);
|
|
46
|
+
if (!id) return null;
|
|
47
|
+
const tab = tabs.find(t => t.getAttribute('aria-controls') === id);
|
|
48
|
+
const panel = panels.find(p => p.id === id);
|
|
49
|
+
return tab && panel ? { tab, panel } : null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const activate = (tabEl) => {
|
|
53
|
+
const id = tabEl.getAttribute('aria-controls');
|
|
54
|
+
const panelEl = root.querySelector(`#${CSS.escape(id)}`);
|
|
55
|
+
if (!panelEl) return;
|
|
56
|
+
|
|
57
|
+
tabs.forEach(t => { t.setAttribute('aria-selected','false'); t.setAttribute('tabindex','-1'); });
|
|
58
|
+
panels.forEach(p => p.hidden = true);
|
|
59
|
+
|
|
60
|
+
tabEl.setAttribute('aria-selected','true');
|
|
61
|
+
tabEl.setAttribute('tabindex','0');
|
|
62
|
+
panelEl.hidden = false;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 初期:ハッシュ優先→なければ先頭
|
|
66
|
+
const initial = byHash();
|
|
67
|
+
activate(initial?.tab ?? tabs[0]);
|
|
68
|
+
|
|
69
|
+
// クリックで切替(ハッシュ書き換えで共有可能)
|
|
70
|
+
root.addEventListener('click', (e) => {
|
|
71
|
+
const a = e.target.closest('[role="tab"]');
|
|
72
|
+
if (!a) return;
|
|
73
|
+
activate(a);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// キーボード(左右で移動)
|
|
77
|
+
root.addEventListener('keydown', (e) => {
|
|
78
|
+
const currentIdx = tabs.findIndex(t => t.getAttribute('aria-selected') === 'true');
|
|
79
|
+
if (currentIdx < 0) return;
|
|
80
|
+
let nextIdx = currentIdx;
|
|
81
|
+
if (e.key === 'ArrowRight') nextIdx = (currentIdx + 1) % tabs.length;
|
|
82
|
+
if (e.key === 'ArrowLeft') nextIdx = (currentIdx - 1 + tabs.length) % tabs.length;
|
|
83
|
+
if (nextIdx !== currentIdx) {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
const next = tabs[nextIdx];
|
|
86
|
+
next.focus();
|
|
87
|
+
activate(next);
|
|
88
|
+
// ハッシュも同期
|
|
89
|
+
const id = next.getAttribute('aria-controls');
|
|
90
|
+
history.replaceState(null, '', `#${id}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ハッシュ変化にも追従(外部リンクからの遷移など)
|
|
95
|
+
window.addEventListener('hashchange', () => {
|
|
96
|
+
const pair = byHash();
|
|
97
|
+
if (pair) activate(pair.tab);
|
|
98
|
+
});
|
|
99
|
+
})();
|
|
100
|
+
</script>
|
|
101
|
+
)}
|
package/Tag.astro
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Tag: ラベル・ステータス表示
|
|
4
|
+
* props: { as?: 'span'|'a', href?: string, class?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { as = 'span', href = '#', class: klass = '' } = Astro.props;
|
|
7
|
+
const cls = `mz-tag ${klass}`.trim();
|
|
8
|
+
---
|
|
9
|
+
{as === 'a' ? (
|
|
10
|
+
<a class={cls} href={href}><slot /></a>
|
|
11
|
+
) : (
|
|
12
|
+
<span class={cls}><slot /></span>
|
|
13
|
+
)}
|
package/Text.astro
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Text: 本文・補足テキスト
|
|
4
|
+
* props: { as?: 'p'|'span'|'small', muted?: boolean, size?: 'sm'|'md'|'lg', class?: string }
|
|
5
|
+
*/
|
|
6
|
+
const { as = 'p', muted = false, size = 'md', class: klass = '' } = Astro.props;
|
|
7
|
+
const sizeMap = { sm: 'var(--text-sm)', md: 'var(--text-md)', lg: 'var(--text-lg)' };
|
|
8
|
+
const style = `font-size:${sizeMap[size] ?? sizeMap.md};${muted ? 'opacity:.8' : ''}`;
|
|
9
|
+
---
|
|
10
|
+
{as === 'span' && (<span style={style} class={klass}><slot /></span>)}
|
|
11
|
+
{as === 'small' && (<small style={style} class={klass}><slot /></small>)}
|
|
12
|
+
{!['span','small'].includes(as) && (<p style={style} class={klass}><slot /></p>)}
|
package/Textarea.astro
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Textarea
|
|
4
|
+
* props: {
|
|
5
|
+
* id: string, rows?: number, value?: string,
|
|
6
|
+
* placeholder?: string, invalid?: boolean, required?: boolean,
|
|
7
|
+
* describedBy?: string, klass?: string
|
|
8
|
+
* }
|
|
9
|
+
*/
|
|
10
|
+
const {
|
|
11
|
+
id,
|
|
12
|
+
rows = 4,
|
|
13
|
+
value,
|
|
14
|
+
placeholder,
|
|
15
|
+
invalid = false,
|
|
16
|
+
required = false,
|
|
17
|
+
describedBy,
|
|
18
|
+
klass = "",
|
|
19
|
+
} = Astro.props;
|
|
20
|
+
|
|
21
|
+
const textValue = (value ?? "").toString();
|
|
22
|
+
---
|
|
23
|
+
<textarea
|
|
24
|
+
id={id}
|
|
25
|
+
class={`mz-textarea ${klass}`.trim()}
|
|
26
|
+
rows={rows}
|
|
27
|
+
placeholder={placeholder}
|
|
28
|
+
aria-invalid={invalid ? "true" : "false"}
|
|
29
|
+
aria-describedby={describedBy}
|
|
30
|
+
required={required}
|
|
31
|
+
>{textValue}</textarea>
|
package/Toaster.astro
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Toaster: 画面端にトーストを積む受け皿(1ページ1回だけ設置)
|
|
4
|
+
* props:
|
|
5
|
+
* - position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' (default: 'bottom-right')
|
|
6
|
+
* - max?: number // 同時表示上限(default: 4)
|
|
7
|
+
* - interactive?: boolean // falseでも静的DOMとしては描画される
|
|
8
|
+
*/
|
|
9
|
+
const { position = "bottom-right", max = 4, interactive = true } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
<div
|
|
12
|
+
id="mz-toaster"
|
|
13
|
+
class={`mz-toaster ${position}`}
|
|
14
|
+
aria-live="polite"
|
|
15
|
+
aria-atomic="true"
|
|
16
|
+
data-max={max}
|
|
17
|
+
></div>
|
|
18
|
+
|
|
19
|
+
{interactive && (
|
|
20
|
+
<script is:inline>
|
|
21
|
+
(() => {
|
|
22
|
+
const host = document.getElementById('mz-toaster');
|
|
23
|
+
if (!host) return;
|
|
24
|
+
|
|
25
|
+
const MAX = Number(host.dataset.max || 4);
|
|
26
|
+
|
|
27
|
+
// 上限超過を同期的に整理(アニメ無しで即 remove)
|
|
28
|
+
const trim = () => {
|
|
29
|
+
while (host.children.length > MAX - 1) {
|
|
30
|
+
host.firstElementChild?.remove();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 公開 API
|
|
35
|
+
window.mzToast = function(message, opts) {
|
|
36
|
+
opts = opts || {};
|
|
37
|
+
const variant = opts.variant || 'info';
|
|
38
|
+
const duration = Number(opts.duration ?? 3500);
|
|
39
|
+
const dismissible = opts.dismissible !== false;
|
|
40
|
+
const title = opts.title || '';
|
|
41
|
+
|
|
42
|
+
// まず整理(ハング回避の肝)
|
|
43
|
+
trim();
|
|
44
|
+
|
|
45
|
+
const el = document.createElement('div');
|
|
46
|
+
el.className = `mz-toast ${variant}`;
|
|
47
|
+
el.setAttribute('role', 'status');
|
|
48
|
+
el.setAttribute('tabindex', '0');
|
|
49
|
+
el.innerHTML = `
|
|
50
|
+
<div class="mz-toast-body">
|
|
51
|
+
${title ? `<strong class="mz-toast-title">${title}</strong>` : ''}
|
|
52
|
+
<div class="mz-toast-message"></div>
|
|
53
|
+
</div>
|
|
54
|
+
${dismissible ? `<button type="button" class="mz-toast-close" aria-label="閉じる">×</button>` : ''}
|
|
55
|
+
`;
|
|
56
|
+
el.querySelector('.mz-toast-message').textContent = message;
|
|
57
|
+
|
|
58
|
+
// 二重クローズ防止
|
|
59
|
+
let closed = false;
|
|
60
|
+
const forceRemove = () => { if (closed) return; closed = true; el.remove(); };
|
|
61
|
+
const animateOut = () => {
|
|
62
|
+
if (closed) return;
|
|
63
|
+
closed = true;
|
|
64
|
+
el.classList.add('leaving');
|
|
65
|
+
el.addEventListener('animationend', () => el.remove(), { once: true });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 追加 & 自動クローズ
|
|
69
|
+
host.appendChild(el);
|
|
70
|
+
if (opts.focus) el.focus();
|
|
71
|
+
if (duration > 0) {
|
|
72
|
+
setTimeout(animateOut, duration);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 操作で閉じる
|
|
76
|
+
el.addEventListener('click', (e) => {
|
|
77
|
+
if (e.target.closest('.mz-toast-close')) animateOut();
|
|
78
|
+
});
|
|
79
|
+
el.addEventListener('keydown', (e) => {
|
|
80
|
+
if (e.key === 'Escape') animateOut();
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
})();
|
|
84
|
+
</script>
|
|
85
|
+
)}
|
package/Tooltip.astro
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Tooltip (CSS only)
|
|
4
|
+
* props: { text: string, position?: 'top'|'right'|'bottom'|'left', klass?: string }
|
|
5
|
+
* ※ フォーカスでも出るように button/a などフォーカス可能要素で包むと良い
|
|
6
|
+
*/
|
|
7
|
+
const { text, position = "top", klass = "" } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
<span class={`mz-tooltip ${position} ${klass}`.trim()} aria-label={text}>
|
|
10
|
+
<slot />
|
|
11
|
+
</span>
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mezzanine-stack/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"files": [
|
|
10
|
+
"*.astro"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
"./Accordion.astro": "./Accordion.astro",
|
|
14
|
+
"./Alert.astro": "./Alert.astro",
|
|
15
|
+
"./Breadcrumbs.astro": "./Breadcrumbs.astro",
|
|
16
|
+
"./Button.astro": "./Button.astro",
|
|
17
|
+
"./Card.astro": "./Card.astro",
|
|
18
|
+
"./Checkbox.astro": "./Checkbox.astro",
|
|
19
|
+
"./Container.astro": "./Container.astro",
|
|
20
|
+
"./EmptyState.astro": "./EmptyState.astro",
|
|
21
|
+
"./Field.astro": "./Field.astro",
|
|
22
|
+
"./Footer.astro": "./Footer.astro",
|
|
23
|
+
"./Header.astro": "./Header.astro",
|
|
24
|
+
"./Heading.astro": "./Heading.astro",
|
|
25
|
+
"./Input.astro": "./Input.astro",
|
|
26
|
+
"./Modal.astro": "./Modal.astro",
|
|
27
|
+
"./Pagination.astro": "./Pagination.astro",
|
|
28
|
+
"./Radio.astro": "./Radio.astro",
|
|
29
|
+
"./Section.astro": "./Section.astro",
|
|
30
|
+
"./Select.astro": "./Select.astro",
|
|
31
|
+
"./Table.astro": "./Table.astro",
|
|
32
|
+
"./Tabs.astro": "./Tabs.astro",
|
|
33
|
+
"./Tag.astro": "./Tag.astro",
|
|
34
|
+
"./Text.astro": "./Text.astro",
|
|
35
|
+
"./Textarea.astro": "./Textarea.astro",
|
|
36
|
+
"./Toaster.astro": "./Toaster.astro",
|
|
37
|
+
"./Tooltip.astro": "./Tooltip.astro"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"astro": ">=4.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@mezzanine-stack/tokens": "0.1.0",
|
|
44
|
+
"@mezzanine-stack/css": "0.1.0"
|
|
45
|
+
}
|
|
46
|
+
}
|