@kubohiroya/turbowarp-extension-data-editor 0.2.1 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +4 -2
  2. package/dist/build-info.json +4 -4
  3. package/dist/config/feature-flags.d.ts +5 -0
  4. package/dist/config/feature-flags.js +6 -0
  5. package/dist/form-presentation.d.ts +8 -0
  6. package/dist/form-presentation.js +61 -0
  7. package/dist/layout-view.d.ts +21 -0
  8. package/dist/layout-view.js +127 -0
  9. package/dist/layout.d.ts +31 -0
  10. package/dist/layout.js +63 -0
  11. package/dist/react.css +126 -0
  12. package/dist/react.d.ts +5 -0
  13. package/dist/react.js +10 -2
  14. package/dist/rjsf-templates.d.ts +18 -0
  15. package/dist/rjsf-templates.js +199 -0
  16. package/dist/rjsf.d.ts +11 -8
  17. package/dist/rjsf.js +35 -12
  18. package/dist/schema-presentation.d.ts +10 -0
  19. package/dist/schema-presentation.js +109 -0
  20. package/dist/source-editor.d.ts +2 -1
  21. package/dist/source-editor.js +23 -5
  22. package/dist/source-view.d.ts +1 -1
  23. package/dist/source-view.js +222 -122
  24. package/dist/turbowarp/appearance.d.ts +1 -1
  25. package/dist/turbowarp/appearance.js +23 -5
  26. package/dist/turbowarp/bridge.js +5 -1
  27. package/dist/turbowarp.d.ts +2 -1
  28. package/dist/turbowarp.js +3 -2
  29. package/dist/webpack4/metafile.json +162 -21
  30. package/dist/webpack4/react.js +12 -6
  31. package/dist/webpack4/react.js.map +2 -2
  32. package/dist/webpack4/rjsf.js +1117 -261
  33. package/dist/webpack4/rjsf.js.map +4 -4
  34. package/dist/webpack4/turbowarp.js +22 -5
  35. package/dist/webpack4/turbowarp.js.map +2 -2
  36. package/docs/README.md +1 -0
  37. package/docs/api.md +7 -0
  38. package/docs/common-layout.ja.md +117 -0
  39. package/docs/package-distribution.md +5 -0
  40. package/package.json +1 -7
package/README.md CHANGED
@@ -6,6 +6,8 @@ TurboWarp拡張が扱う構造化データを、フォームや独自UIで編集
6
6
 
7
7
  日本語の利用手順・API・GUI接続・公開・検証記録は[ドキュメント一覧](docs/README.md)へ集約しています。
8
8
 
9
+ タブ・グループ・グリッドは[共通レイアウトガイド](docs/common-layout.ja.md)を参照してください。標準RJSFとSource/Formで同じJSON宣言を使用し、ホストの起動時フラグ `commonLayout` で有効にします(既定OFF)。
10
+
9
11
  [公開エディター・日英案内](https://kubohiroya.github.io/turbowarp-extension-data-editor/) と
10
12
  [公開・検証の運用Issue #26](https://github.com/kubohiroya/turbowarp-extension-data-editor/issues/26) も本リポジトリが所有します。
11
13
 
@@ -103,10 +105,10 @@ pnpm test
103
105
  `pnpm test` は型宣言を含むビルド後、コアと標準フォームのテストを実行します。
104
106
  パッケージは ESM です。ブラウザには ES2022 の組み込み API が必要です。
105
107
  標準フォームの React / ReactDOM peer は 16.14 / 17 / 18。GUI は既存の 16.14 を共有します。
106
- 初回の実験版は `@kubohiroya/turbowarp-extension-data-editor@0.1.0` です。固定SchemaのSource/Form編集は `0.2.0` で追加しました。今回の配布版 `0.2.1` は、既定OFFの固定入力契約・標準field transaction・固定フォームとGUI接続も含みます。
108
+ 初回の実験版は `@kubohiroya/turbowarp-extension-data-editor@0.1.0` です。固定SchemaのSource/Form編集は `0.2.0`、既定OFFの固定入力契約・標準field transaction・固定フォームとGUI接続は `0.2.1` で追加しました。配布版 `0.2.2` は、編集見出しのブロック名・アイコン、項目の削除メニュー・追加ボタン、key-valueの2行表示を含みます。
107
109
 
108
110
  ```sh
109
- npm install --save-exact @kubohiroya/turbowarp-extension-data-editor@0.2.1
111
+ npm install --save-exact @kubohiroya/turbowarp-extension-data-editor@0.2.2
110
112
  ```
111
113
 
112
114
  配布物・改名時の互換性・公開手順は[パッケージ配布](docs/package-distribution.md)を参照してください。
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-extension-data-editor",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "repository": "https://github.com/kubohiroya/turbowarp-extension-data-editor",
5
- "revision": "485f0e550e622235624b5e9176820161aeeba252",
6
- "sourceState": "working-tree",
7
- "sourceSha256": "175674eaaffce60dd9ebcf4c31f0f02a3bbd9dc6b8659ab3101ea6d8bceeea2f",
5
+ "revision": "9409867dc58d56cd652afa83f2361810dc8da826",
6
+ "sourceState": "commit",
7
+ "sourceSha256": "7aae735fe9bd33f5fcc3ca055186be84aa89de1fb6b31c66b68d1cca3c3731f3",
8
8
  "capabilities": {
9
9
  "preparedSource": 1
10
10
  }
@@ -0,0 +1,5 @@
1
+ /** Captured once by the host at service/editor creation; never controlled by extension metadata. */
2
+ export interface EditorFeatureFlags {
3
+ readonly commonLayout?: boolean;
4
+ }
5
+ export declare function captureFeatureFlags(options?: EditorFeatureFlags): Readonly<Required<EditorFeatureFlags>>;
@@ -0,0 +1,6 @@
1
+ export function captureFeatureFlags(options = {}) {
2
+ if (options.commonLayout !== undefined && typeof options.commonLayout !== 'boolean') {
3
+ throw new TypeError('Invalid common layout feature flag.');
4
+ }
5
+ return Object.freeze({ commonLayout: options.commonLayout === true });
6
+ }
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ /** Explicit schema/ui titles remain untouched; fallback property keys become readable labels. */
3
+ export declare function propertyLabel(key: string): string;
4
+ export declare function FormHelp({ description, id }: {
5
+ description: unknown;
6
+ id?: string;
7
+ }): React.ReactElement | null;
8
+ export declare const formStyles = "\n[data-editor-form]{--editor-accent:var(--block-editor-accent,#426ad6);--editor-line:var(--block-editor-border,#dde3ed);--editor-muted:var(--block-editor-muted,var(--block-editor-text,#637086));color:var(--block-editor-text,#25334a);font:14px/1.5 system-ui,sans-serif;min-width:0}\n[data-editor-form] *,[data-editor-form] *:before,[data-editor-form] *:after{box-sizing:border-box}\n[data-editor-form] fieldset{min-width:0}\n[data-editor-form] fieldset.editor-form-binding,[data-editor-form] .editor-form-section,[data-editor-form] .rjsf fieldset{padding:0;border:0;margin:0 0 18px}\n[data-editor-form] .editor-form-section>legend,[data-editor-form] .rjsf fieldset>legend{font-size:15px;font-weight:650;padding:0;margin:0 0 10px;color:inherit}\n[data-editor-form] .editor-form-field,[data-editor-form] .rjsf .form-group{margin:0 0 14px;min-width:0}\n[data-editor-form] .editor-form-field>label,[data-editor-form] .rjsf .control-label{display:block;font-size:13px;font-weight:600;margin:0 0 6px;color:inherit}\n[data-editor-form] .required{color:var(--editor-accent);margin-left:3px}\n[data-editor-form] input:not([type=checkbox]):not([type=radio]),[data-editor-form] select,[data-editor-form] textarea{display:block;width:100%;min-height:38px;margin:0;padding:8px 11px;border:1px solid var(--editor-line);border-radius:7px;background:var(--block-editor-input,white);color:inherit;font:inherit;box-shadow:0 1px 2px #172b4d05}\n[data-editor-form] textarea{min-height:76px;resize:vertical}\n[data-editor-form] input:focus-visible,[data-editor-form] select:focus-visible,[data-editor-form] textarea:focus-visible,[data-editor-form] button:focus-visible,[data-editor-form] summary:focus-visible{outline:2px solid var(--editor-accent);outline-offset:2px}\n[data-editor-form] input[readonly]{background:var(--block-editor-subtle,var(--block-editor-background,#f5f7fb));color:var(--editor-muted)}\n[data-editor-form] input[type=checkbox]{width:17px;height:17px;margin:0;accent-color:var(--editor-accent)}\n[data-editor-form] .editor-form-field>label.editor-form-toggle,[data-editor-form] .rjsf .checkbox>label{display:flex;align-items:center;gap:8px;font-weight:500;margin:8px 0}\n[data-editor-form] button{font:inherit;cursor:pointer;border:1px solid var(--editor-line);border-radius:6px;background:var(--block-editor-input,white);color:inherit;padding:6px 10px;margin:0}\n[data-editor-form] button:disabled{cursor:default;opacity:.5}\n[data-editor-form] .editor-form-hint,[data-editor-form] .editor-form-help{color:var(--editor-muted);font-size:12px;line-height:1.5;margin:5px 0 10px}\n[data-editor-form] .editor-form-help>summary{cursor:pointer;width:fit-content;font-weight:500}\n[data-editor-form] .editor-form-help p{margin:8px 0}\n[data-editor-form] .block-editor-layout-tabs>[role=tablist]{display:flex;gap:4px;flex-wrap:wrap;padding:5px;background:var(--block-editor-subtle,var(--block-editor-background,#f1f4f9));border:1px solid var(--editor-line);border-radius:9px;margin:4px 0 20px}\n[data-editor-form] .block-editor-layout-tabs>[role=tablist]>button{border:0;border-radius:6px;background:transparent;color:var(--editor-muted);padding:8px 18px;margin:0;font-weight:550;text-decoration:none;min-width:76px}\n[data-editor-form] .block-editor-layout-tabs>[role=tablist]>[aria-selected=true]{background:var(--block-editor-input,white);color:var(--editor-accent);box-shadow:0 1px 4px #1d30431a}\n[data-editor-form] .editor-form-error-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;margin-left:7px;padding:0 5px;border-radius:10px;background:#fff0f0;color:#b53d45;font-size:11px}\n[data-editor-form] fieldset.block-editor-layout-group{border:1px solid var(--editor-line);border-radius:10px;padding:16px 18px 4px;margin:0 0 18px;background:var(--block-editor-background,white)}\n[data-editor-form] fieldset.block-editor-layout-group>legend{padding:0 6px;font-size:13px;font-weight:650}\n[data-editor-form] .block-editor-layout-grid{display:grid;gap:0 18px;grid-template-columns:repeat(var(--editor-columns),minmax(0,1fr));align-items:start}\n[data-editor-form] .block-editor-layout-grid>*{min-width:0}\n[data-editor-form] .block-editor-layout-tabs>[hidden]{display:none!important}\n[data-editor-form] .editor-form-toolbar{display:flex;align-items:start;justify-content:space-between;gap:12px;margin:0 0 18px;padding-bottom:14px;border-bottom:1px solid var(--editor-line)}\n[data-editor-form] .editor-form-modes{display:flex;gap:3px;border:1px solid var(--editor-line);border-radius:8px;padding:3px;background:var(--block-editor-subtle,var(--block-editor-background,#f1f4f9))}\n[data-editor-form] .editor-form-modes>button{border:0;border-radius:5px;padding:5px 14px;background:transparent;color:var(--editor-muted);margin:0}\n[data-editor-form] .editor-form-modes>[aria-pressed=true]{background:var(--block-editor-input,white);color:var(--editor-accent);box-shadow:0 1px 3px #1d30431a}\n[data-editor-form] .editor-form-tools{font-size:12px;color:var(--editor-muted);margin:0 0 12px}\n[data-editor-form] .editor-form-tools>summary{cursor:pointer;padding:5px 0;list-style-position:inside}\n[data-editor-form] .editor-form-tools[open]{padding:10px 12px;border:1px solid var(--editor-line);border-radius:8px;background:var(--block-editor-subtle,var(--block-editor-background,#f7f9fc))}\n[data-editor-form] .editor-form-tools button{border:1px solid var(--editor-line);border-radius:6px;background:var(--block-editor-input,white);padding:5px 9px;margin:5px 5px 5px 0;color:inherit}\n[data-editor-form] .editor-form-tools label{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:5px 0}\n[data-editor-form] .editor-form-invalid{font-size:12px;color:#b53d45;margin:5px 0}\n[data-editor-form] [data-source-error=true]>input,[data-editor-form] [data-source-error=true]>textarea{border-color:#b53d45}\n[data-editor-form] .editor-form-array-item{display:flex;align-items:start;gap:10px}\n[data-editor-form] .editor-form-array-item>.editor-form-field{flex:1;min-width:0}\n@media(max-width:600px){[data-editor-form] .block-editor-layout-grid{grid-template-columns:minmax(0,1fr)}[data-editor-form] fieldset.block-editor-layout-group{padding:12px 12px 0}[data-editor-form] .block-editor-layout-tabs>[role=tablist]>button{padding:7px 12px;min-width:60px}[data-editor-form] .editor-form-toolbar{flex-wrap:wrap}}\n";
@@ -0,0 +1,61 @@
1
+ import * as React from 'react';
2
+ /** Explicit schema/ui titles remain untouched; fallback property keys become readable labels. */
3
+ export function propertyLabel(key) {
4
+ if (/^[a-z]{1,3}$/.test(key))
5
+ return key;
6
+ const text = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').replace(/\b(id|uri|url|json)\b/gi, word => word.toUpperCase());
7
+ return text ? text[0].toUpperCase() + text.slice(1) : '設定';
8
+ }
9
+ export function FormHelp({ description, id }) {
10
+ if (typeof description !== 'string' || !description)
11
+ return null;
12
+ return description.length > 130 ? React.createElement("details", { className: "editor-form-help", id: id },
13
+ React.createElement("summary", null, "\u8AAC\u660E"),
14
+ React.createElement("p", null, description))
15
+ : React.createElement("p", { className: "editor-form-hint", id: id }, description);
16
+ }
17
+ // Scoped to the opt-in form root; the same rules ship with both rendering paths.
18
+ export const formStyles = `
19
+ [data-editor-form]{--editor-accent:var(--block-editor-accent,#426ad6);--editor-line:var(--block-editor-border,#dde3ed);--editor-muted:var(--block-editor-muted,var(--block-editor-text,#637086));color:var(--block-editor-text,#25334a);font:14px/1.5 system-ui,sans-serif;min-width:0}
20
+ [data-editor-form] *,[data-editor-form] *:before,[data-editor-form] *:after{box-sizing:border-box}
21
+ [data-editor-form] fieldset{min-width:0}
22
+ [data-editor-form] fieldset.editor-form-binding,[data-editor-form] .editor-form-section,[data-editor-form] .rjsf fieldset{padding:0;border:0;margin:0 0 18px}
23
+ [data-editor-form] .editor-form-section>legend,[data-editor-form] .rjsf fieldset>legend{font-size:15px;font-weight:650;padding:0;margin:0 0 10px;color:inherit}
24
+ [data-editor-form] .editor-form-field,[data-editor-form] .rjsf .form-group{margin:0 0 14px;min-width:0}
25
+ [data-editor-form] .editor-form-field>label,[data-editor-form] .rjsf .control-label{display:block;font-size:13px;font-weight:600;margin:0 0 6px;color:inherit}
26
+ [data-editor-form] .required{color:var(--editor-accent);margin-left:3px}
27
+ [data-editor-form] input:not([type=checkbox]):not([type=radio]),[data-editor-form] select,[data-editor-form] textarea{display:block;width:100%;min-height:38px;margin:0;padding:8px 11px;border:1px solid var(--editor-line);border-radius:7px;background:var(--block-editor-input,white);color:inherit;font:inherit;box-shadow:0 1px 2px #172b4d05}
28
+ [data-editor-form] textarea{min-height:76px;resize:vertical}
29
+ [data-editor-form] input:focus-visible,[data-editor-form] select:focus-visible,[data-editor-form] textarea:focus-visible,[data-editor-form] button:focus-visible,[data-editor-form] summary:focus-visible{outline:2px solid var(--editor-accent);outline-offset:2px}
30
+ [data-editor-form] input[readonly]{background:var(--block-editor-subtle,var(--block-editor-background,#f5f7fb));color:var(--editor-muted)}
31
+ [data-editor-form] input[type=checkbox]{width:17px;height:17px;margin:0;accent-color:var(--editor-accent)}
32
+ [data-editor-form] .editor-form-field>label.editor-form-toggle,[data-editor-form] .rjsf .checkbox>label{display:flex;align-items:center;gap:8px;font-weight:500;margin:8px 0}
33
+ [data-editor-form] button{font:inherit;cursor:pointer;border:1px solid var(--editor-line);border-radius:6px;background:var(--block-editor-input,white);color:inherit;padding:6px 10px;margin:0}
34
+ [data-editor-form] button:disabled{cursor:default;opacity:.5}
35
+ [data-editor-form] .editor-form-hint,[data-editor-form] .editor-form-help{color:var(--editor-muted);font-size:12px;line-height:1.5;margin:5px 0 10px}
36
+ [data-editor-form] .editor-form-help>summary{cursor:pointer;width:fit-content;font-weight:500}
37
+ [data-editor-form] .editor-form-help p{margin:8px 0}
38
+ [data-editor-form] .block-editor-layout-tabs>[role=tablist]{display:flex;gap:4px;flex-wrap:wrap;padding:5px;background:var(--block-editor-subtle,var(--block-editor-background,#f1f4f9));border:1px solid var(--editor-line);border-radius:9px;margin:4px 0 20px}
39
+ [data-editor-form] .block-editor-layout-tabs>[role=tablist]>button{border:0;border-radius:6px;background:transparent;color:var(--editor-muted);padding:8px 18px;margin:0;font-weight:550;text-decoration:none;min-width:76px}
40
+ [data-editor-form] .block-editor-layout-tabs>[role=tablist]>[aria-selected=true]{background:var(--block-editor-input,white);color:var(--editor-accent);box-shadow:0 1px 4px #1d30431a}
41
+ [data-editor-form] .editor-form-error-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;margin-left:7px;padding:0 5px;border-radius:10px;background:#fff0f0;color:#b53d45;font-size:11px}
42
+ [data-editor-form] fieldset.block-editor-layout-group{border:1px solid var(--editor-line);border-radius:10px;padding:16px 18px 4px;margin:0 0 18px;background:var(--block-editor-background,white)}
43
+ [data-editor-form] fieldset.block-editor-layout-group>legend{padding:0 6px;font-size:13px;font-weight:650}
44
+ [data-editor-form] .block-editor-layout-grid{display:grid;gap:0 18px;grid-template-columns:repeat(var(--editor-columns),minmax(0,1fr));align-items:start}
45
+ [data-editor-form] .block-editor-layout-grid>*{min-width:0}
46
+ [data-editor-form] .block-editor-layout-tabs>[hidden]{display:none!important}
47
+ [data-editor-form] .editor-form-toolbar{display:flex;align-items:start;justify-content:space-between;gap:12px;margin:0 0 18px;padding-bottom:14px;border-bottom:1px solid var(--editor-line)}
48
+ [data-editor-form] .editor-form-modes{display:flex;gap:3px;border:1px solid var(--editor-line);border-radius:8px;padding:3px;background:var(--block-editor-subtle,var(--block-editor-background,#f1f4f9))}
49
+ [data-editor-form] .editor-form-modes>button{border:0;border-radius:5px;padding:5px 14px;background:transparent;color:var(--editor-muted);margin:0}
50
+ [data-editor-form] .editor-form-modes>[aria-pressed=true]{background:var(--block-editor-input,white);color:var(--editor-accent);box-shadow:0 1px 3px #1d30431a}
51
+ [data-editor-form] .editor-form-tools{font-size:12px;color:var(--editor-muted);margin:0 0 12px}
52
+ [data-editor-form] .editor-form-tools>summary{cursor:pointer;padding:5px 0;list-style-position:inside}
53
+ [data-editor-form] .editor-form-tools[open]{padding:10px 12px;border:1px solid var(--editor-line);border-radius:8px;background:var(--block-editor-subtle,var(--block-editor-background,#f7f9fc))}
54
+ [data-editor-form] .editor-form-tools button{border:1px solid var(--editor-line);border-radius:6px;background:var(--block-editor-input,white);padding:5px 9px;margin:5px 5px 5px 0;color:inherit}
55
+ [data-editor-form] .editor-form-tools label{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:5px 0}
56
+ [data-editor-form] .editor-form-invalid{font-size:12px;color:#b53d45;margin:5px 0}
57
+ [data-editor-form] [data-source-error=true]>input,[data-editor-form] [data-source-error=true]>textarea{border-color:#b53d45}
58
+ [data-editor-form] .editor-form-array-item{display:flex;align-items:start;gap:10px}
59
+ [data-editor-form] .editor-form-array-item>.editor-form-field{flex:1;min-width:0}
60
+ @media(max-width:600px){[data-editor-form] .block-editor-layout-grid{grid-template-columns:minmax(0,1fr)}[data-editor-form] fieldset.block-editor-layout-group{padding:12px 12px 0}[data-editor-form] .block-editor-layout-tabs>[role=tablist]>button{padding:7px 12px;min-width:60px}[data-editor-form] .editor-form-toolbar{flex-wrap:wrap}}
61
+ `;
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ import type { EditorViewContext } from './types.js';
3
+ export declare const useCommonLayout: () => boolean;
4
+ /** One environment for the entire editor; saving reveals errors without changing the draft. */
5
+ export declare function LayoutHost({ enabled, context, children }: {
6
+ enabled: boolean;
7
+ context: EditorViewContext;
8
+ children: React.ReactNode;
9
+ }): React.ReactElement;
10
+ export interface LayoutField {
11
+ readonly name: string;
12
+ readonly content: React.ReactNode;
13
+ readonly errors?: number;
14
+ }
15
+ /** Share layout behavior while leaving each renderer in charge of its controls. */
16
+ export declare function ObjectLayout({ ui, fields, knownFields, fallback }: {
17
+ ui: unknown;
18
+ fields: readonly LayoutField[];
19
+ knownFields: readonly string[];
20
+ fallback: React.ReactNode;
21
+ }): React.ReactElement;
@@ -0,0 +1,127 @@
1
+ import * as React from 'react';
2
+ import { formStyles } from './form-presentation.js';
3
+ import { resolveEditorLayout } from './layout.js';
4
+ const Environment = React.createContext({ enabled: false, reveal: 0, disabled: false });
5
+ export const useCommonLayout = () => React.useContext(Environment).enabled;
6
+ let nextId = 0;
7
+ /** One environment for the entire editor; saving reveals errors without changing the draft. */
8
+ export function LayoutHost({ enabled, context, children }) {
9
+ const [reveal, setReveal] = React.useState(0);
10
+ const [status, setStatus] = React.useState(context.getState().status);
11
+ const root = React.useRef(null);
12
+ React.useLayoutEffect(() => {
13
+ let saving = context.getState().status === 'saving';
14
+ let active = true;
15
+ const stop = context.subscribe(() => {
16
+ const state = context.getState();
17
+ const failed = saving && state.status === 'editing' && state.issues.length > 0;
18
+ saving = state.status === 'saving';
19
+ setStatus(state.status);
20
+ if (failed)
21
+ void Promise.resolve().then(() => { if (active)
22
+ setReveal(n => n + 1); });
23
+ });
24
+ return () => { active = false; stop(); };
25
+ }, [context]);
26
+ React.useEffect(() => {
27
+ if (!enabled || !reveal)
28
+ return;
29
+ const timer = setTimeout(() => {
30
+ var _a, _b, _c;
31
+ const controls = (_a = root.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('[data-layout-error="true"] input, [data-layout-error="true"] textarea, [data-layout-error="true"] select');
32
+ const candidates = Array.from(controls !== null && controls !== void 0 ? controls : []).filter(control => !control.closest('[hidden]') && !control.matches(':disabled') && !control.hasAttribute('data-source-type-control'));
33
+ const target = (_c = (_b = candidates.find(control => { var _a; return ((_a = control.closest('[data-source-path]')) === null || _a === void 0 ? void 0 : _a.getAttribute('data-source-error')) === 'true'; })) !== null && _b !== void 0 ? _b : candidates.find(control => { var _a; return (_a = control.closest('.form-group')) === null || _a === void 0 ? void 0 : _a.classList.contains('has-error'); })) !== null && _c !== void 0 ? _c : candidates.find(control => { var _a; return ((_a = control.closest('[data-layout-field]')) === null || _a === void 0 ? void 0 : _a.getAttribute('data-layout-error')) === 'true'; });
34
+ target === null || target === void 0 ? void 0 : target.focus();
35
+ }, 0);
36
+ return () => clearTimeout(timer);
37
+ }, [enabled, reveal]);
38
+ return React.createElement(Environment.Provider, { value: { enabled, reveal, disabled: status !== 'editing' } }, enabled ? React.createElement("div", { ref: root, "data-editor-form": "" },
39
+ React.createElement("style", null, formStyles),
40
+ children) : children);
41
+ }
42
+ function names(items) {
43
+ return items.flatMap(item => 'field' in item ? [item.field] : 'tabs' in item ? item.tabs.flatMap(tab => names(tab.items)) : names(item.items));
44
+ }
45
+ function Items({ items, fields }) {
46
+ return React.createElement(React.Fragment, null, items.map((item, index) => {
47
+ if ('field' in item) {
48
+ const field = fields.get(item.field);
49
+ return field ? React.createElement("div", { key: index, "data-layout-field": item.field, "data-layout-error": !!field.errors }, field.content) : null;
50
+ }
51
+ if ('tabs' in item)
52
+ return React.createElement(Tabs, { key: index, tabs: item.tabs, fields: fields });
53
+ if ('group' in item)
54
+ return React.createElement("fieldset", { className: "block-editor-layout-group", key: index },
55
+ React.createElement("legend", null, item.group),
56
+ React.createElement(Items, { items: item.items, fields: fields }));
57
+ return React.createElement("div", { key: index, className: "block-editor-layout-grid", style: { '--editor-columns': item.grid } },
58
+ React.createElement(Items, { items: item.items, fields: fields }));
59
+ }));
60
+ }
61
+ function Tabs({ tabs, fields }) {
62
+ const { reveal, disabled } = React.useContext(Environment);
63
+ const [selected, select] = React.useState(0);
64
+ const [id] = React.useState(() => `block-editor-layout-${++nextId}`);
65
+ const buttons = React.useRef([]);
66
+ const counts = tabs.map(tab => names(tab.items).reduce((total, name) => { var _a, _b; return total + ((_b = (_a = fields.get(name)) === null || _a === void 0 ? void 0 : _a.errors) !== null && _b !== void 0 ? _b : 0); }, 0));
67
+ const errors = counts.join(',');
68
+ const handled = React.useRef(0);
69
+ React.useEffect(() => {
70
+ if (reveal <= handled.current)
71
+ return;
72
+ const first = counts.findIndex(count => count > 0);
73
+ if (first >= 0) {
74
+ handled.current = reveal;
75
+ select(first);
76
+ }
77
+ // Wait for delayed RJSF error propagation, then consume this save attempt once.
78
+ }, [reveal, errors]);
79
+ const current = Math.min(selected, tabs.length - 1);
80
+ return React.createElement("div", { className: "block-editor-layout-tabs", "data-layout-errors": errors },
81
+ React.createElement("div", { role: "tablist", "aria-label": "\u30D5\u30A9\u30FC\u30E0\u306E\u9805\u76EE" }, tabs.map((tab, index) => React.createElement("button", { key: index, ref: element => { buttons.current[index] = element; }, type: "button", role: "tab", id: `${id}-tab-${index}`, "aria-controls": `${id}-panel-${index}`, "aria-selected": current === index, disabled: disabled, tabIndex: current === index ? 0 : -1, onClick: () => select(index), onKeyDown: event => {
82
+ var _a;
83
+ if (event.nativeEvent.isComposing || event.keyCode === 229)
84
+ return;
85
+ let target;
86
+ switch (event.key) {
87
+ case 'ArrowRight':
88
+ target = (index + 1) % tabs.length;
89
+ break;
90
+ case 'ArrowLeft':
91
+ target = (index + tabs.length - 1) % tabs.length;
92
+ break;
93
+ case 'Home':
94
+ target = 0;
95
+ break;
96
+ case 'End':
97
+ target = tabs.length - 1;
98
+ break;
99
+ default: return;
100
+ }
101
+ event.preventDefault();
102
+ event.stopPropagation();
103
+ select(target);
104
+ (_a = buttons.current[target]) === null || _a === void 0 ? void 0 : _a.focus();
105
+ } },
106
+ tab.label,
107
+ counts[index] ? React.createElement("span", { className: "editor-form-error-badge", "aria-label": `エラー ${counts[index]} 件` }, counts[index]) : null))),
108
+ tabs.map((tab, index) => React.createElement("div", { key: index, role: "tabpanel", id: `${id}-panel-${index}`, "aria-labelledby": `${id}-tab-${index}`, hidden: current !== index },
109
+ React.createElement(Items, { items: tab.items, fields: fields }))));
110
+ }
111
+ /** Share layout behavior while leaving each renderer in charge of its controls. */
112
+ export function ObjectLayout({ ui, fields, knownFields, fallback }) {
113
+ const { enabled } = React.useContext(Environment);
114
+ const resolution = enabled ? resolveEditorLayout(ui, knownFields) : { kind: 'absent' };
115
+ if (resolution.kind === 'absent')
116
+ return React.createElement(React.Fragment, null, fallback);
117
+ if (resolution.kind === 'invalid')
118
+ return React.createElement(React.Fragment, null,
119
+ React.createElement("p", { role: "status", "data-layout-diagnostic": "" },
120
+ "\u30EC\u30A4\u30A2\u30A6\u30C8\u3092\u9069\u7528\u3067\u304D\u307E\u305B\u3093: ",
121
+ resolution.diagnostic),
122
+ fallback);
123
+ const map = new Map(fields.map(field => [field.name, field]));
124
+ return React.createElement(React.Fragment, null,
125
+ React.createElement(Items, { items: resolution.items, fields: map }),
126
+ fields.filter(field => !resolution.fields.has(field.name)).map(field => React.createElement("div", { key: field.name, "data-layout-field": field.name, "data-layout-error": !!field.errors }, field.content)));
127
+ }
@@ -0,0 +1,31 @@
1
+ /** JSON-only presentation metadata. Field names are literal immediate object keys, never paths. */
2
+ export type EditorLayoutItem = {
3
+ readonly field: string;
4
+ } | {
5
+ readonly group: string;
6
+ readonly items: readonly EditorLayoutItem[];
7
+ } | {
8
+ readonly grid: number;
9
+ readonly items: readonly EditorLayoutItem[];
10
+ } | {
11
+ readonly tabs: readonly {
12
+ readonly label: string;
13
+ readonly items: readonly EditorLayoutItem[];
14
+ }[];
15
+ };
16
+ export type EditorLayout = {
17
+ readonly version: 1;
18
+ readonly items: readonly EditorLayoutItem[];
19
+ };
20
+ export type LayoutResolution = {
21
+ readonly kind: 'absent';
22
+ } | {
23
+ readonly kind: 'invalid';
24
+ readonly diagnostic: string;
25
+ } | {
26
+ readonly kind: 'layout';
27
+ readonly items: readonly EditorLayoutItem[];
28
+ readonly fields: ReadonlySet<string>;
29
+ };
30
+ /** Validate the whole declaration before moving any controls. Invalid metadata never hides fields. */
31
+ export declare function resolveEditorLayout(ui: unknown, knownFields: readonly string[]): LayoutResolution;
package/dist/layout.js ADDED
@@ -0,0 +1,63 @@
1
+ const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
2
+ /** Validate the whole declaration before moving any controls. Invalid metadata never hides fields. */
3
+ export function resolveEditorLayout(ui, knownFields) {
4
+ if (!record(ui) || !record(ui['ui:options']) || !Object.hasOwn(ui['ui:options'], 'extensionDataLayout'))
5
+ return { kind: 'absent' };
6
+ const input = ui['ui:options'].extensionDataLayout;
7
+ const known = new Set(knownFields), fields = new Set();
8
+ let count = 0;
9
+ const fail = (message) => { throw new TypeError(message); };
10
+ const keys = (node, expected) => {
11
+ if (Object.keys(node).some(key => !expected.includes(key)))
12
+ fail('Unknown layout option.');
13
+ };
14
+ const label = (text) => typeof text === 'string' && text.trim().length > 0 && text.length <= 128 ? text : fail('Invalid layout label.');
15
+ const items = (value, depth) => {
16
+ if (depth > 8 || !Array.isArray(value) || value.length > 256)
17
+ return fail('Layout limit exceeded.');
18
+ return value.map((node) => {
19
+ if (++count > 256 || !record(node))
20
+ return fail('Invalid layout item or limit exceeded.');
21
+ if (Object.hasOwn(node, 'field')) {
22
+ keys(node, ['field']);
23
+ if (typeof node.field !== 'string' || !known.has(node.field))
24
+ return fail('Unknown layout field.');
25
+ if (fields.has(node.field))
26
+ return fail('Duplicate layout field.');
27
+ fields.add(node.field);
28
+ return { field: node.field };
29
+ }
30
+ if (Object.hasOwn(node, 'group')) {
31
+ keys(node, ['group', 'items']);
32
+ return { group: label(node.group), items: items(node.items, depth + 1) };
33
+ }
34
+ if (Object.hasOwn(node, 'grid')) {
35
+ keys(node, ['grid', 'items']);
36
+ if (typeof node.grid !== 'number' || !Number.isInteger(node.grid) || node.grid < 1 || node.grid > 4)
37
+ return fail('Grid columns must be 1–4.');
38
+ return { grid: node.grid, items: items(node.items, depth + 1) };
39
+ }
40
+ if (Object.hasOwn(node, 'tabs')) {
41
+ keys(node, ['tabs']);
42
+ if (!Array.isArray(node.tabs) || !node.tabs.length || node.tabs.length > 16)
43
+ return fail('Invalid tab count.');
44
+ return { tabs: node.tabs.map((tab) => {
45
+ if (++count > 256 || !record(tab))
46
+ return fail('Invalid tab or limit exceeded.');
47
+ keys(tab, ['label', 'items']);
48
+ return { label: label(tab.label), items: items(tab.items, depth + 1) };
49
+ }) };
50
+ }
51
+ return fail('Unknown layout item.');
52
+ });
53
+ };
54
+ try {
55
+ if (!record(input) || input.version !== 1)
56
+ return fail('Unsupported layout version.');
57
+ keys(input, ['version', 'items']);
58
+ return { kind: 'layout', items: items(input.items, 0), fields };
59
+ }
60
+ catch (error) {
61
+ return { kind: 'invalid', diagnostic: error instanceof Error ? error.message : 'Invalid layout.' };
62
+ }
63
+ }
package/dist/react.css CHANGED
@@ -8,6 +8,31 @@
8
8
  width: min(900px, calc(100vw - 32px));
9
9
  }
10
10
 
11
+ .modal-content .header {
12
+ height: auto;
13
+ min-height: 3.5rem;
14
+ align-items: center;
15
+ }
16
+
17
+ .modal-content .header > div:first-child {
18
+ flex: 1 1 0;
19
+ min-width: 0;
20
+ margin: 0;
21
+ overflow-wrap: anywhere;
22
+ text-align: center;
23
+ }
24
+
25
+ .modal-content .header > div:last-child {
26
+ flex: 0 0 auto;
27
+ }
28
+
29
+ .modal-content .header img {
30
+ flex-shrink: 0;
31
+ width: 1.75rem;
32
+ height: 1.75rem;
33
+ object-fit: contain;
34
+ }
35
+
11
36
  .tree {
12
37
  min-width: 0;
13
38
  }
@@ -152,6 +177,107 @@
152
177
  margin-bottom: 1rem;
153
178
  }
154
179
 
180
+ .body :global([data-block-editor-item]) {
181
+ box-sizing: border-box;
182
+ min-width: 0;
183
+ margin: 0.75rem 0;
184
+ padding: 1rem;
185
+ border: 1px solid var(--block-editor-border, rgba(0, 0, 0, 0.15));
186
+ border-radius: var(--block-editor-radius, 0.5rem);
187
+ }
188
+
189
+ .body :global([data-block-editor-item-heading]),
190
+ .body :global([data-block-editor-item-actions]) {
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 0.5rem;
194
+ }
195
+
196
+ .body :global([data-block-editor-item-heading]) {
197
+ justify-content: space-between;
198
+ margin-bottom: 0.75rem;
199
+ font-weight: bold;
200
+ }
201
+
202
+ .body :global([data-block-editor-item]) input,
203
+ .body :global([data-block-editor-item]) select,
204
+ .body :global([data-block-editor-item]) textarea {
205
+ box-sizing: border-box;
206
+ }
207
+
208
+ .body :global([data-block-editor-pair-row]) {
209
+ display: grid;
210
+ grid-template-columns: 3rem minmax(0, 1fr);
211
+ align-items: center;
212
+ gap: 0.5rem 0.75rem;
213
+ margin-top: 0.5rem;
214
+ }
215
+
216
+ .body :global([data-block-editor-pair-row]) > label {
217
+ grid-column: 1;
218
+ margin: 0;
219
+ }
220
+
221
+ .body :global([data-block-editor-pair-row]) > :not(label) {
222
+ grid-column: 2;
223
+ min-width: 0;
224
+ }
225
+
226
+ .body :global([data-block-editor-pair-row]) input:not([type="checkbox"]):not([type="radio"]),
227
+ .body :global([data-block-editor-pair-row]) select,
228
+ .body :global([data-block-editor-pair-row]) textarea {
229
+ margin-top: 0;
230
+ }
231
+
232
+ .body :global([data-block-editor-item-actions]) button,
233
+ .body :global([data-block-editor-item-menu]) button,
234
+ .body :global([data-block-editor-add-item]) {
235
+ min-height: 2.25rem;
236
+ padding: 0.5rem 0.75rem;
237
+ border: 1px solid var(--block-editor-border, rgba(0, 0, 0, 0.15));
238
+ border-radius: var(--block-editor-radius, 0.5rem);
239
+ color: var(--block-editor-text, #333);
240
+ background: var(--block-editor-button, white);
241
+ cursor: pointer;
242
+ font: inherit;
243
+ }
244
+
245
+ .body :global([data-block-editor-item-menu]) {
246
+ position: relative;
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ .body :global([data-block-editor-item-menu]) :global([role="menu"]) {
251
+ position: absolute;
252
+ inset-inline-end: 0;
253
+ top: 100%;
254
+ z-index: 1;
255
+ min-width: max-content;
256
+ padding: 0.25rem;
257
+ background: var(--block-editor-background, white);
258
+ border: 1px solid var(--block-editor-border, rgba(0, 0, 0, 0.15));
259
+ border-radius: var(--block-editor-radius, 0.5rem);
260
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
261
+ }
262
+
263
+ .body :global([data-block-editor-item-menu]) :global([role="menuitem"]) {
264
+ width: 100%;
265
+ border: 0;
266
+ text-align: start;
267
+ }
268
+
269
+ .body :global([data-block-editor-item-actions]) button:disabled,
270
+ .body :global([data-block-editor-add-item]):disabled {
271
+ opacity: 0.5;
272
+ cursor: default;
273
+ }
274
+
275
+ .body :global([data-block-editor-item-menu]) button:focus-visible,
276
+ .body :global([data-block-editor-add-item]):focus-visible {
277
+ outline: 2px solid currentColor;
278
+ outline-offset: 2px;
279
+ }
280
+
155
281
  /* The adapter marks only controls belonging to the field's own RJSF errors. */
156
282
  .body :global(.rjsf) input:global([data-block-editor-invalid="true"]),
157
283
  .body :global(.rjsf) select:global([data-block-editor-invalid="true"]),
package/dist/react.d.ts CHANGED
@@ -10,6 +10,9 @@ export interface DialogSessionState {
10
10
  readonly error: unknown;
11
11
  }
12
12
  export interface DialogSession {
13
+ readonly reference?: {
14
+ readonly blockId: string;
15
+ };
13
16
  getState(): DialogSessionState;
14
17
  getFixedInputView?(): unknown;
15
18
  subscribe(listener: () => void): () => void;
@@ -43,6 +46,8 @@ export interface EditorModalProps {
43
46
  readonly id: string;
44
47
  readonly className: string;
45
48
  readonly contentLabel: string;
49
+ readonly headerImage?: string;
50
+ readonly headerClassName?: string | undefined;
46
51
  readonly onRequestClose: () => void;
47
52
  readonly children?: React.ReactNode;
48
53
  }
package/dist/react.js CHANGED
@@ -203,6 +203,7 @@ export class BlockEditorDialog extends React.Component {
203
203
  }
204
204
  }
205
205
  render() {
206
+ var _a, _b, _c, _d;
206
207
  const { styles, messages, ModalComponent, session } = this.props;
207
208
  const saving = this.state.status === 'saving';
208
209
  const hierarchy = typeof session.getNodes === 'function';
@@ -211,11 +212,18 @@ export class BlockEditorDialog extends React.Component {
211
212
  try {
212
213
  fixed = Boolean(session.getFixedInputView());
213
214
  }
214
- catch (_a) {
215
+ catch (_e) {
215
216
  fixed = true;
216
217
  }
217
218
  }
218
- return React.createElement(ModalComponent, { className: `${styles.modalContent || ''} ${hierarchy ? styles.hierarchyModal : ''} ${this.props.className || ''}`, contentLabel: messages.title, id: "blockEditorModal", onRequestClose: this.handleClose },
219
+ const root = (_a = session.getNodes) === null || _a === void 0 ? void 0 : _a.call(session).find(node => node.inputPath.length === 0);
220
+ const rootId = ((_b = session.reference) === null || _b === void 0 ? void 0 : _b.blockId) || (root === null || root === void 0 ? void 0 : root.reference.blockId);
221
+ const appearance = rootId ? (_c = this.props.nodeAppearances) === null || _c === void 0 ? void 0 : _c[rootId] : undefined;
222
+ const name = (appearance === null || appearance === void 0 ? void 0 : appearance.name) || (rootId ? (_d = this.props.nodeLabels) === null || _d === void 0 ? void 0 : _d[rootId] : undefined) || (root === null || root === void 0 ? void 0 : root.opcode);
223
+ const title = name ? messages.title.includes('{name}') ? messages.title.replace('{name}', () => name) :
224
+ /ブロック|block/i.test(messages.title) ? messages.title.replace(/ブロック|block/i, () => name) :
225
+ `${name}: ${messages.title}` : messages.title;
226
+ return React.createElement(ModalComponent, { className: `${styles.modalContent || ''} ${hierarchy ? styles.hierarchyModal : ''} ${this.props.className || ''}`, contentLabel: title, ...((appearance === null || appearance === void 0 ? void 0 : appearance.iconURI) ? { headerImage: appearance.iconURI } : {}), headerClassName: styles.header, id: "blockEditorModal", onRequestClose: this.handleClose },
219
227
  React.createElement("div", { "aria-busy": saving, className: styles.body, onCompositionStart: () => { this.composing = true; }, onCompositionEnd: () => { this.composing = false; }, onKeyDownCapture: event => {
220
228
  if (this.composing || event.nativeEvent.isComposing || event.keyCode === 229)
221
229
  event.stopPropagation();
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import type { ArrayFieldTemplateItemType, ArrayFieldTemplateProps, ArrayFieldTitleProps, DescriptionFieldProps, FieldTemplateProps, IconButtonProps, ObjectFieldTemplateProps, WidgetProps, WrapIfAdditionalTemplateProps } from '@rjsf/utils';
3
+ /** Reuse the v5 object template's title, description and add-property controls. */
4
+ export declare function layoutObjectTemplate(DefaultTemplate: React.ComponentType<ObjectFieldTemplateProps>): (props: ObjectFieldTemplateProps) => React.ReactElement;
5
+ export declare function compactDescriptionTemplate(DefaultTemplate: React.ComponentType<DescriptionFieldProps>): (props: DescriptionFieldProps) => React.ReactElement;
6
+ export declare function compactArrayTitleTemplate(DefaultTemplate: React.ComponentType<ArrayFieldTitleProps>): (props: ArrayFieldTitleProps) => React.ReactElement;
7
+ export declare function compactArrayTemplate(DefaultTemplate: React.ComponentType<ArrayFieldTemplateProps>): (props: ArrayFieldTemplateProps) => React.ReactElement;
8
+ /** Boolean widgets own their visible label instead of using the surrounding FieldTemplate. */
9
+ export declare function compactCheckboxWidget(DefaultWidget: React.ComponentType<WidgetProps>): (props: WidgetProps) => React.ReactElement;
10
+ /** Keep the property key out of the value's visible label. */
11
+ export declare function valueFieldTemplate(DefaultTemplate: React.ComponentType<FieldTemplateProps>, flag: string): (props: FieldTemplateProps) => React.ReactElement;
12
+ /** Keep RJSF's object layout while numbering each editable additional property. */
13
+ export declare function numberedObjectTemplate(DefaultTemplate: React.ComponentType<ObjectFieldTemplateProps>, flag: string): (props: ObjectFieldTemplateProps) => React.ReactElement;
14
+ export declare function additionalItemTemplate(flag: string): (props: WrapIfAdditionalTemplateProps) => React.ReactElement;
15
+ /** Deletion is available only after explicitly opening the item's menu. */
16
+ export declare function ItemMenu({ disabled, onClick }: Pick<IconButtonProps, 'disabled' | 'onClick'>): React.ReactElement;
17
+ export declare function AddItemButton({ disabled, onClick, className }: IconButtonProps): React.ReactElement;
18
+ export declare function ArrayItem(props: ArrayFieldTemplateItemType): React.ReactElement;