mokkun 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/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # mokkun
2
+
3
+ YAML形式のUI構造定義から画面モックアップを生成・表示するブラウザツール
4
+
5
+ ## 概要
6
+
7
+ | 項目 | 内容 |
8
+ |------|------|
9
+ | 実行環境 | ブラウザのみ(サーバー不要) |
10
+ | YAML読込 | ファイル選択、URLパラメータ、ドラッグ&ドロップ |
11
+ | テーマ管理 | Light / Dark |
12
+ | 画面遷移 | リンククリックで遷移 |
13
+
14
+ ## 技術スタック
15
+
16
+ | カテゴリ | 技術 |
17
+ |----------|------|
18
+ | 言語 | TypeScript |
19
+ | ビルド | Vite |
20
+ | テスト | Vitest |
21
+ | YAMLパーサー | js-yaml |
22
+ | スタイル | CSS Variables + テーマファイル |
23
+
24
+ ## セットアップ
25
+
26
+ ```bash
27
+ # 依存関係のインストール
28
+ pnpm install
29
+
30
+ # 開発サーバー起動
31
+ pnpm dev
32
+
33
+ # ビルド
34
+ pnpm build
35
+
36
+ # テスト実行
37
+ pnpm test
38
+ ```
39
+
40
+ ## ディレクトリ構造
41
+
42
+ ```
43
+ mokkun/
44
+ ├── src/
45
+ │ ├── main.ts # エントリーポイント
46
+ │ ├── lib.ts # ライブラリエクスポート
47
+ │ ├── parser/ # YAMLパーサー
48
+ │ ├── renderer/ # レンダリングエンジン
49
+ │ │ ├── screen-renderer.ts
50
+ │ │ ├── action-handler.ts
51
+ │ │ ├── components/ # UIコンポーネント
52
+ │ │ └── utils/ # ユーティリティ
53
+ │ ├── theme/ # テーマ管理
54
+ │ ├── loader/ # ファイル読み込み
55
+ │ ├── types/ # 型定義
56
+ │ └── __tests__/ # テスト
57
+ ├── themes/ # テーマ設定
58
+ ├── schema.json # JSON Schema定義
59
+ └── index.html
60
+ ```
61
+
62
+ ## サポートコンポーネント
63
+
64
+ ### フォーム要素
65
+
66
+ | コンポーネント | 説明 |
67
+ |---------------|------|
68
+ | text | テキスト入力 |
69
+ | number | 数値入力 |
70
+ | textarea | テキストエリア |
71
+ | select | セレクトボックス |
72
+ | multi_select | 複数選択セレクト |
73
+ | combobox | 検索可能なセレクト |
74
+ | radio_group | ラジオボタングループ |
75
+ | checkbox | チェックボックス |
76
+ | checkbox_group | チェックボックスグループ |
77
+ | toggle | トグルスイッチ |
78
+ | date_picker | 日付選択 |
79
+ | time_picker | 時間選択 |
80
+ | duration_picker | 期間選択 |
81
+ | duration_input | 期間入力 |
82
+ | file_upload | ファイルアップロード |
83
+ | image_uploader | 画像アップローダー |
84
+ | calendar | カレンダー日付選択 |
85
+ | browser | 階層構造データの単一選択 |
86
+
87
+ ### データ表示
88
+
89
+ | コンポーネント | 説明 |
90
+ |---------------|------|
91
+ | data_table | データテーブル |
92
+ | pagination | ページネーション |
93
+ | badge | バッジ |
94
+ | chip | チップ |
95
+ | status_label | ステータスラベル |
96
+ | timeline | タイムライン |
97
+ | definition_list | 定義リスト |
98
+
99
+ ### レイアウト・ナビゲーション
100
+
101
+ | コンポーネント | 説明 |
102
+ |---------------|------|
103
+ | app_header | アプリケーションヘッダー(ロゴ、テナント切替、ユーザーメニュー) |
104
+ | app_navi | アプリナビゲーション(主要機能の切り替え) |
105
+ | heading | 見出し |
106
+ | tabs | タブ |
107
+ | accordion_panel | アコーディオン |
108
+ | disclosure | 開閉コンテンツ |
109
+ | wizard / stepper | ウィザード / ステッパー |
110
+ | section_nav | セクションナビゲーション |
111
+ | float_area | フローティング領域 |
112
+ | segmented_control | セグメントコントロール |
113
+
114
+ ### フィードバック
115
+
116
+ | コンポーネント | 説明 |
117
+ |---------------|------|
118
+ | tooltip | ツールチップ |
119
+ | loader | ローダー |
120
+ | notification_bar | 通知バー |
121
+ | response_message | レスポンスメッセージ |
122
+ | information_panel | 情報パネル |
123
+ | line_clamp | 行数制限テキスト |
124
+
125
+ ### その他
126
+
127
+ | コンポーネント | 説明 |
128
+ |---------------|------|
129
+ | dropdown | ドロップダウンメニュー |
130
+ | repeater | 動的フィールドグループ |
131
+ | google_map_embed | Googleマップ埋め込み |
132
+ | photo_manager | 写真管理 |
133
+ | delete_confirm_dialog | 削除確認ダイアログ |
134
+
135
+ ## YAML構造
136
+
137
+ ```yaml
138
+ view:
139
+ screen_name:
140
+ title: "画面タイトル"
141
+ description: "画面の説明"
142
+ fields:
143
+ - id: "field_id"
144
+ type: "text"
145
+ label: "ラベル"
146
+ required: true
147
+ actions:
148
+ - id: "submit"
149
+ type: "submit"
150
+ label: "送信"
151
+ style: "primary"
152
+
153
+ common_components:
154
+ component_name:
155
+ type: "field_group"
156
+ fields: []
157
+
158
+ validations:
159
+ rule_name:
160
+ rules:
161
+ required: true
162
+ min: 1
163
+ ```
164
+
165
+ ## 使用方法
166
+
167
+ ### ライブラリビルド
168
+
169
+ ```bash
170
+ # ライブラリとしてビルド
171
+ pnpm build:lib
172
+ ```
173
+
174
+ ### HTMLでの使用例
175
+
176
+ ```html
177
+ <!DOCTYPE html>
178
+ <html>
179
+ <head>
180
+ <title>画面モックアップ</title>
181
+ <!-- CSSを読み込む -->
182
+ <link rel="stylesheet" href="./dist/mokkun.css">
183
+ </head>
184
+ <body>
185
+ <div id="mokkun-app"></div>
186
+ <script type="module">
187
+ import { Mokkun } from './dist/mokkun.esm.js';
188
+
189
+ // init()はPromiseを返す
190
+ Mokkun.init({
191
+ container: '#mokkun-app',
192
+ yamlUrl: './screens.yaml',
193
+ theme: 'light',
194
+ onReady: (instance) => {
195
+ console.log('Mokkun ready:', instance.getScreenNames());
196
+ },
197
+ onError: (error) => {
198
+ console.error('Mokkun error:', error);
199
+ }
200
+ });
201
+ </script>
202
+ </body>
203
+ </html>
204
+ ```
205
+
206
+ ### UMD形式(グローバル変数として使用)
207
+
208
+ ```html
209
+ <!DOCTYPE html>
210
+ <html>
211
+ <head>
212
+ <title>画面モックアップ</title>
213
+ <link rel="stylesheet" href="./dist/mokkun.css">
214
+ </head>
215
+ <body>
216
+ <div id="mokkun-app"></div>
217
+ <script src="./dist/mokkun.js"></script>
218
+ <script>
219
+ // グローバル変数Mokkunが利用可能
220
+ Mokkun.init({
221
+ container: '#mokkun-app',
222
+ yamlUrl: './screens.yaml',
223
+ theme: 'light'
224
+ });
225
+ </script>
226
+ </body>
227
+ </html>
228
+ ```
229
+
230
+ ### インラインYAMLの使用
231
+
232
+ ```javascript
233
+ const yamlContent = `
234
+ view:
235
+ login:
236
+ title: ログイン
237
+ fields:
238
+ - id: email
239
+ type: text
240
+ label: メールアドレス
241
+ required: true
242
+ - id: password
243
+ type: text
244
+ label: パスワード
245
+ required: true
246
+ actions:
247
+ - id: submit
248
+ type: submit
249
+ label: ログイン
250
+ style: primary
251
+ `;
252
+
253
+ const instance = await Mokkun.init({
254
+ container: '#mokkun-app',
255
+ yamlContent: yamlContent,
256
+ theme: 'dark'
257
+ });
258
+
259
+ // 画面を切り替える
260
+ instance.showScreen('login');
261
+
262
+ ```
263
+
264
+ ### サンプルファイル
265
+
266
+ `examples/`フォルダに動作するサンプルが用意されています:
267
+
268
+ | ファイル | 説明 |
269
+ |---------|------|
270
+ | `esm-example.html` | ESM形式でMokkunを読み込むサンプル |
271
+ | `umd-example.html` | UMD形式(グローバル変数)でMokkunを読み込むサンプル |
272
+ | `inline-yaml-example.html` | JavaScriptでYAMLを直接定義するサンプル |
273
+ | `screens.yaml` | サンプル画面定義(ログイン、新規登録、ダッシュボード) |
274
+
275
+ ```bash
276
+ # ローカルで確認する場合
277
+ python3 -m http.server 8080
278
+ # http://localhost:8080/examples/esm-example.html を開く
279
+ ```
280
+
281
+ ## ライセンス
282
+
283
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from 'node:http'
4
+ import { readFileSync, existsSync, statSync } from 'node:fs'
5
+ import { resolve, dirname, extname, basename } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { exec } from 'node:child_process'
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url))
10
+ const distDir = resolve(__dirname, '../dist')
11
+
12
+ // Parse arguments
13
+ const args = process.argv.slice(2)
14
+ let yamlFile = null
15
+ let port = 3333
16
+
17
+ for (let i = 0; i < args.length; i++) {
18
+ if (args[i] === '--port' || args[i] === '-p') {
19
+ port = parseInt(args[i + 1], 10)
20
+ i++
21
+ } else if (args[i] === '--help' || args[i] === '-h') {
22
+ printHelp()
23
+ process.exit(0)
24
+ } else if (!args[i].startsWith('-')) {
25
+ yamlFile = resolve(process.cwd(), args[i])
26
+ }
27
+ }
28
+
29
+ function printHelp() {
30
+ console.log(`
31
+ mokkun - YAML-based form & mockup viewer
32
+
33
+ Usage:
34
+ npx mokkun [options] [yaml-file]
35
+
36
+ Options:
37
+ -p, --port <port> Port number (default: 3333)
38
+ -h, --help Show this help
39
+
40
+ Examples:
41
+ npx mokkun # Start with built-in sample
42
+ npx mokkun ./my-form.yaml # Open specific YAML file
43
+ npx mokkun -p 8080 form.yaml # Custom port
44
+ `)
45
+ }
46
+
47
+ // Validate YAML file if specified
48
+ if (yamlFile) {
49
+ if (!existsSync(yamlFile)) {
50
+ console.error(`Error: File not found: ${yamlFile}`)
51
+ process.exit(1)
52
+ }
53
+ if (!yamlFile.endsWith('.yaml') && !yamlFile.endsWith('.yml')) {
54
+ console.error(`Error: File must be .yaml or .yml: ${yamlFile}`)
55
+ process.exit(1)
56
+ }
57
+ }
58
+
59
+ // Check dist directory exists
60
+ if (!existsSync(resolve(distDir, 'mokkun.js'))) {
61
+ console.error('Error: dist/mokkun.js not found. Run "pnpm build:lib" first.')
62
+ process.exit(1)
63
+ }
64
+
65
+ // MIME types
66
+ const MIME_TYPES = {
67
+ '.html': 'text/html; charset=utf-8',
68
+ '.js': 'application/javascript; charset=utf-8',
69
+ '.mjs': 'application/javascript; charset=utf-8',
70
+ '.css': 'text/css; charset=utf-8',
71
+ '.yaml': 'text/yaml; charset=utf-8',
72
+ '.yml': 'text/yaml; charset=utf-8',
73
+ '.json': 'application/json; charset=utf-8',
74
+ '.map': 'application/json; charset=utf-8',
75
+ }
76
+
77
+ // Generate viewer HTML
78
+ function generateViewerHtml() {
79
+ const yamlParam = yamlFile ? `yaml=${encodeURIComponent('/__yaml__/' + basename(yamlFile))}` : ''
80
+ const viewerHtmlPath = resolve(__dirname, 'viewer.html')
81
+ let html = readFileSync(viewerHtmlPath, 'utf-8')
82
+ if (yamlParam) {
83
+ html = html.replace('/* __YAML_URL__ */ null', JSON.stringify('/__yaml__/' + basename(yamlFile)))
84
+ }
85
+ return html
86
+ }
87
+
88
+ // Serve files
89
+ const server = createServer((req, res) => {
90
+ const url = new URL(req.url, `http://localhost:${port}`)
91
+ const pathname = url.pathname
92
+
93
+ // Root -> viewer HTML
94
+ if (pathname === '/' || pathname === '/index.html') {
95
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
96
+ res.end(generateViewerHtml())
97
+ return
98
+ }
99
+
100
+ // Serve user YAML file
101
+ if (pathname.startsWith('/__yaml__/') && yamlFile) {
102
+ try {
103
+ const content = readFileSync(yamlFile, 'utf-8')
104
+ res.writeHead(200, { 'Content-Type': 'text/yaml; charset=utf-8' })
105
+ res.end(content)
106
+ } catch {
107
+ res.writeHead(404)
108
+ res.end('Not found')
109
+ }
110
+ return
111
+ }
112
+
113
+ // Serve dist files (mokkun.js, mokkun.css, etc.)
114
+ const distPath = resolve(distDir, pathname.slice(1))
115
+ if (distPath.startsWith(distDir) && existsSync(distPath) && statSync(distPath).isFile()) {
116
+ const ext = extname(distPath)
117
+ const mime = MIME_TYPES[ext] || 'application/octet-stream'
118
+ try {
119
+ const content = readFileSync(distPath)
120
+ res.writeHead(200, { 'Content-Type': mime })
121
+ res.end(content)
122
+ } catch {
123
+ res.writeHead(500)
124
+ res.end('Internal error')
125
+ }
126
+ return
127
+ }
128
+
129
+ res.writeHead(404)
130
+ res.end('Not found')
131
+ })
132
+
133
+ server.listen(port, () => {
134
+ const url = `http://localhost:${port}`
135
+ console.log(`Mokkun viewer running at ${url}`)
136
+ if (yamlFile) {
137
+ console.log(`Serving: ${yamlFile}`)
138
+ }
139
+ console.log('Press Ctrl+C to stop.\n')
140
+
141
+ // Open browser
142
+ openBrowser(url)
143
+ })
144
+
145
+ function openBrowser(url) {
146
+ const platform = process.platform
147
+ const cmd =
148
+ platform === 'darwin' ? 'open' :
149
+ platform === 'win32' ? 'start' :
150
+ 'xdg-open'
151
+
152
+ exec(`${cmd} ${url}`, () => {
153
+ // Ignore errors (e.g., no display server)
154
+ })
155
+ }
@@ -0,0 +1,262 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mokkun</title>
7
+ <link rel="stylesheet" href="/mokkun.css">
8
+ <style>
9
+ * { box-sizing: border-box; }
10
+
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', 'Noto Sans JP', sans-serif;
15
+ background-color: #f5f6f7;
16
+ }
17
+
18
+ .page-container {
19
+ min-height: 100vh;
20
+ display: flex;
21
+ flex-direction: column;
22
+ }
23
+
24
+ .toolbar {
25
+ position: fixed;
26
+ top: 0;
27
+ left: 0;
28
+ right: 0;
29
+ background: #1a1a2e;
30
+ color: #fff;
31
+ padding: 12px 24px;
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 16px;
35
+ z-index: 1000;
36
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
37
+ }
38
+
39
+ .toolbar h1 {
40
+ margin: 0;
41
+ font-size: 16px;
42
+ font-weight: 600;
43
+ }
44
+
45
+ .toolbar-section {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 8px;
49
+ }
50
+
51
+ .toolbar label {
52
+ font-size: 14px;
53
+ color: #a0a0a0;
54
+ }
55
+
56
+ .toolbar select {
57
+ padding: 8px 12px;
58
+ border-radius: 6px;
59
+ border: 1px solid #3a3a5a;
60
+ background: #2a2a4a;
61
+ color: #fff;
62
+ font-size: 14px;
63
+ cursor: pointer;
64
+ min-width: 180px;
65
+ }
66
+
67
+ .toolbar select:hover { border-color: #5a5a8a; }
68
+ .toolbar select:focus {
69
+ outline: none;
70
+ border-color: #6366f1;
71
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
72
+ }
73
+
74
+ .toolbar-spacer { flex: 1; }
75
+
76
+ .theme-toggle {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: 8px;
80
+ }
81
+
82
+ .theme-toggle button {
83
+ padding: 8px 16px;
84
+ border-radius: 6px;
85
+ border: 1px solid #3a3a5a;
86
+ background: transparent;
87
+ color: #fff;
88
+ font-size: 14px;
89
+ cursor: pointer;
90
+ transition: all 0.2s;
91
+ }
92
+
93
+ .theme-toggle button:hover { background: #3a3a5a; }
94
+ .theme-toggle button.active {
95
+ background: #6366f1;
96
+ border-color: #6366f1;
97
+ }
98
+
99
+ #mokkun-app {
100
+ margin-top: 60px;
101
+ flex: 1;
102
+ padding: 24px;
103
+ }
104
+
105
+ .loading {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ height: 50vh;
110
+ font-size: 18px;
111
+ color: #666;
112
+ }
113
+
114
+ .error-message {
115
+ background: #fee2e2;
116
+ border: 1px solid #ef4444;
117
+ border-radius: 8px;
118
+ padding: 16px 24px;
119
+ margin: 24px;
120
+ color: #b91c1c;
121
+ }
122
+
123
+ body.dark-theme { background-color: #1a1a2e; }
124
+ body.dark-theme .toolbar { background: #0f0f1a; }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div class="page-container">
129
+ <div class="toolbar">
130
+ <h1>Mokkun</h1>
131
+
132
+ <div class="toolbar-section">
133
+ <label for="screen-select">Screen:</label>
134
+ <select id="screen-select">
135
+ <option value="">Loading...</option>
136
+ </select>
137
+ </div>
138
+
139
+ <div class="toolbar-spacer"></div>
140
+
141
+ <div class="theme-toggle">
142
+ <button id="light-theme" class="active">Light</button>
143
+ <button id="dark-theme">Dark</button>
144
+ </div>
145
+ </div>
146
+
147
+ <div id="mokkun-app">
148
+ <div class="loading">Loading...</div>
149
+ </div>
150
+ </div>
151
+
152
+ <script src="/mokkun.js"></script>
153
+ <script>
154
+ (function() {
155
+ var mokkunInstance = null;
156
+ var screenSelect = document.getElementById('screen-select');
157
+ var lightThemeBtn = document.getElementById('light-theme');
158
+ var darkThemeBtn = document.getElementById('dark-theme');
159
+ var currentTheme = 'light';
160
+
161
+ // YAML URL injected by CLI (or null for sample)
162
+ var yamlUrl = /* __YAML_URL__ */ null;
163
+
164
+ // Theme
165
+ function setTheme(theme) {
166
+ currentTheme = theme;
167
+ document.body.classList.toggle('dark-theme', theme === 'dark');
168
+ lightThemeBtn.classList.toggle('active', theme === 'light');
169
+ darkThemeBtn.classList.toggle('active', theme === 'dark');
170
+ if (mokkunInstance) mokkunInstance.setTheme(theme);
171
+ }
172
+
173
+ lightThemeBtn.addEventListener('click', function() { setTheme('light'); });
174
+ darkThemeBtn.addEventListener('click', function() { setTheme('dark'); });
175
+
176
+ // Screen change
177
+ screenSelect.addEventListener('change', function() {
178
+ if (mokkunInstance && screenSelect.value) {
179
+ mokkunInstance.showScreen(screenSelect.value);
180
+ }
181
+ });
182
+
183
+ // Sample YAML for when no file is specified
184
+ var sampleYaml = [
185
+ 'view:',
186
+ ' sample:',
187
+ ' title: "Mokkun Sample"',
188
+ ' description: "npx mokkun <yaml-file> to view your own YAML"',
189
+ ' fields:',
190
+ ' - id: name',
191
+ ' type: text',
192
+ ' label: "Name"',
193
+ ' required: true',
194
+ ' - id: email',
195
+ ' type: text',
196
+ ' label: "Email"',
197
+ ' input_type: email',
198
+ ' - id: category',
199
+ ' type: select',
200
+ ' label: "Category"',
201
+ ' options:',
202
+ ' - label: "Select..."',
203
+ ' value: ""',
204
+ ' - label: "Option A"',
205
+ ' value: a',
206
+ ' - label: "Option B"',
207
+ ' value: b',
208
+ ' - id: message',
209
+ ' type: textarea',
210
+ ' label: "Message"',
211
+ ' rows: 4',
212
+ ' actions:',
213
+ ' - id: submit',
214
+ ' type: submit',
215
+ ' label: "Submit"',
216
+ ' style: primary',
217
+ ].join('\n');
218
+
219
+ // Initialize
220
+ async function init() {
221
+ var options = {
222
+ container: '#mokkun-app',
223
+ theme: currentTheme,
224
+ onReady: function(instance) {
225
+ var screens = instance.getScreenNames();
226
+ screenSelect.innerHTML = '';
227
+ screens.forEach(function(name) {
228
+ var opt = document.createElement('option');
229
+ opt.value = name;
230
+ opt.textContent = name;
231
+ screenSelect.appendChild(opt);
232
+ });
233
+ screenSelect.value = screens[0] || '';
234
+ },
235
+ onError: function(error) {
236
+ document.getElementById('mokkun-app').innerHTML =
237
+ '<div class="error-message"><strong>Error:</strong> ' + error.message + '</div>';
238
+ },
239
+ onSubmit: function(screenName, formData) {
240
+ console.log('Submit:', screenName, formData);
241
+ }
242
+ };
243
+
244
+ if (yamlUrl) {
245
+ options.yamlUrl = yamlUrl;
246
+ } else {
247
+ options.yamlContent = sampleYaml;
248
+ }
249
+
250
+ try {
251
+ mokkunInstance = await Mokkun.init(options);
252
+ } catch (e) {
253
+ document.getElementById('mokkun-app').innerHTML =
254
+ '<div class="error-message"><strong>Error:</strong> ' + e.message + '</div>';
255
+ }
256
+ }
257
+
258
+ init();
259
+ })();
260
+ </script>
261
+ </body>
262
+ </html>