@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.
@@ -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>
@@ -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>)}
@@ -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
+ )}
@@ -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
+ }