// UI Components - サイドバー、コンテキストメニュー、テキスト入力

///|
/// サイドバー幅
let sidebar_width : Int = 220

///|
/// メニューボタンのスタイル
let menu_button_style : String = "width: 100%; text-align: left; background: none; border: none; cursor: pointer; padding: 4px 8px;"

///|
/// CSS 変数をヘックス色に変換(カラーピッカー用)
fn to_hex_color(color : String, default : String) -> String {
  if color.has_prefix("var(") {
    default
  } else if color == "transparent" || color == "" {
    default
  } else {
    color
  }
}

///|
/// サイドバーをレンダリング(後方互換性のため残す)
pub fn render_sidebar(
  state : @core.EditorState,
  history : @model.History,
  _canvas_height : Int,
) -> @element.DomNode {
  render_floating_panel(state, history)
}

///|
/// フローティングパネルをレンダリング
pub fn render_floating_panel(
  state : @core.EditorState,
  history : @model.History,
) -> @element.DomNode {
  div(
    dyn_style=fn() {
      let theme = state.get_theme()
      let is_mobile = state.is_mobile.get()
      let is_dark = state.theme_mode.get() == @model.Dark
      let shadow = if is_dark { "rgba(0,0,0,0.4)" } else { "rgba(0,0,0,0.15)" }
      if is_mobile {
        // モバイル: 画面下部にスライド表示
        "position: fixed; bottom: 0; left: 0; right: 0; max-height: 50vh; background: \{theme.ui_bg}; border-top: 1px solid \{theme.ui_border}; border-radius: 12px 12px 0 0; padding: 12px; box-sizing: border-box; overflow-y: auto; box-shadow: 0 -2px 12px \{shadow}; z-index: 100; color: \{theme.ui_text};"
      } else {
        // デスクトップ: 右端にフローティング表示
        "position: fixed; top: 56px; right: 12px; width: \{sidebar_width}px; max-height: calc(100vh - 80px); background: \{theme.ui_bg}; border: 1px solid \{theme.ui_border}; border-radius: 8px; padding: 12px; box-sizing: border-box; overflow-y: auto; box-shadow: 0 2px 12px \{shadow}; z-index: 100; color: \{theme.ui_text};"
      }
    },
    [render_panel_content(state, history)],
  )
}

///|
/// インラインパネルをレンダリング(モーダル内で使用、position: fixed なし)
pub fn render_inline_panel(
  state : @core.EditorState,
  history : @model.History,
) -> @element.DomNode {
  div(
    dyn_style=fn() {
      let theme = state.get_theme()
      "padding: 8px; background: \{theme.ui_bg}; color: \{theme.ui_text}; font-size: 12px;"
    },
    [render_panel_content(state, history)],
  )
}

///|
/// パネルの共通コンテンツ
fn render_panel_content(
  state : @core.EditorState,
  history : @model.History,
) -> @element.DomNode {
  @element.fragment([
    // パンくずリスト(階層表示)
    render_breadcrumb(state),
    // 選択中の要素の詳細を表示(単一選択の場合)
    show(fn() { state.selected_ids.get().length() == 1 }, fn() {
      render_element_details(state, history)
    }),
    // 複数選択の場合
    show(fn() { state.selected_ids.get().length() > 1 }, fn() {
      render_multi_selection_panel(state)
    }),
    // 選択なしの場合:Canvas設定と要素ツリーを表示
    show(fn() { state.selected_ids.get().length() == 0 }, fn() {
      render_canvas_root_compact(state)
    }),
  ])
}

///|
/// パンくずリストをレンダリング
fn render_breadcrumb(state : @core.EditorState) -> @element.DomNode {
  let theme = state.get_theme()
  let breadcrumb_style = "display: flex; align-items: center; gap: 4px; margin-bottom: 12px; border-bottom: 1px solid \{theme.ui_border}; padding-bottom: 8px; font-size: 12px;"
  let link_style = "color: \{theme.ui_accent}; cursor: pointer; background: none; border: none; padding: 0; font-size: 12px;"
  let current_style = "color: \{theme.ui_text}; font-weight: bold;"
  // 選択なしの場合はパンくずを非表示
  show(fn() { state.selected_ids.get().length() > 0 }, fn() {
    div(style=breadcrumb_style, [
      // Canvas リンク
      button(
        style=link_style,
        on=events().click(fn(_) { state.select(None) }),
        [text("Canvas")],
      ),
      @element.span(style="color: #999;", [text(" > ")]),
      // 選択中の要素名
      show(fn() { state.selected_ids.get().length() == 1 }, fn() {
        @element.span(style=current_style, [
          text_dyn(fn() {
            match state.get_selected_id() {
              Some(id) =>
                match state.find_element(id) {
                  Some(el) => @model.shape_name(el.shape) + " #" + el.id
                  None => "Unknown"
                }
              None => "Unknown"
            }
          }),
        ])
      }),
      // 複数選択の場合
      show(fn() { state.selected_ids.get().length() > 1 }, fn() {
        @element.span(style=current_style, [
          text_dyn(fn() { "\{state.selected_ids.get().length()} selected" }),
        ])
      }),
    ])
  })
}

///|
/// 複数選択時のパネルをレンダリング
fn render_multi_selection_panel(state : @core.EditorState) -> @element.DomNode {
  let theme = state.get_theme()
  let item_style = "display: flex; align-items: center; gap: 4px; padding: 4px 8px; cursor: pointer; border-radius: 3px; font-size: 11px;"
  div(style="font-size: 12px;", [
    // ヘッダー
    div(
      style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;",
      [
        @element.span(style="font-weight: 500; color: \{theme.ui_text};", [
          text_dyn(fn() {
            "\{state.selected_ids.get().length()} elements selected"
          }),
        ]),
        // 選択解除ボタン
        button(
          style="padding: 2px 6px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; font-size: 10px; background: #fff;",
          on=events().click(fn(_) { state.select(None) }),
          [text("Clear")],
        ),
      ],
    ),
    // 選択中の要素リスト
    div(
      style="max-height: 200px; overflow-y: auto; border: 1px solid \{theme.ui_border}; border-radius: 4px; padding: 4px;",
      [
        @element.for_each(fn() { state.selected_ids.get() }, fn(id, _idx) {
          let el_opt = state.find_element(id)
          let shape_icon = match el_opt {
            Some(el) =>
              match el.shape {
                @model.Rect(_, _, _, _) => "▢"
                @model.Circle(_) => "○"
                @model.Ellipse(_, _) => "⬭"
                @model.Line(_, _) => "╱"
                @model.Polyline(_) => "⟋"
                @model.Path(_, _, _, _, _) => "✎"
                @model.Text(_, _) => "T"
              }
            None => "?"
          }
          let shape_name = match el_opt {
            Some(el) => @model.shape_name(el.shape)
            None => "Unknown"
          }
          div(
            style=item_style,
            on=events().click(fn(_) { state.select(Some(id)) }),
            [
              @element.span(style="font-size: 14px; width: 16px;", [
                text(shape_icon),
              ]),
              @element.span(style="color: #666;", [text(shape_name)]),
              @element.span(style="color: #999; font-family: monospace;", [
                text("#" + id),
              ]),
              // 選択から除外ボタン
              button(
                style="margin-left: auto; padding: 1px 4px; border: 1px solid #ccc; border-radius: 2px; cursor: pointer; font-size: 9px; background: #fff; color: #666;",
                on=events().click(fn(e) {
                  let _ = e.as_any()._call("stopPropagation", [])
                  state.remove_from_selection(id)
                }),
                [text("×")],
              ),
            ],
          )
        }),
      ],
    ),
    // ヒントテキスト
    div(
      style="margin-top: 8px; font-size: 10px; color: \{theme.ui_text_muted};",
      [text("Tip: Shift+click to add/remove, Ctrl+C/V to copy/paste")],
    ),
  ])
}

///|
/// アコーディオンヘッダーのスタイル
let accordion_header_style : String = "display: flex; align-items: center; justify-content: space-between; padding: 8px; background: #f0f0f0; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold; color: #333; margin-bottom: 4px; user-select: none;"

///|
/// Canvas ルート表示(コンパクト版 - アコーディオンは閉じた状態で開始)
fn render_canvas_root_compact(state : @core.EditorState) -> @element.DomNode {
  let settings_open = @luna.signal(false) // 閉じた状態で開始
  let elements_open = @luna.signal(true) // Elements は開いた状態
  div(style="display: flex; flex-direction: column; gap: 4px;", [
    // Canvas Settings アコーディオン(閉じた状態)
    div(style="", [
      div(
        style=accordion_header_style,
        on=events().click(fn(_) { settings_open.update(fn(v) { not(v) }) }),
        [
          text("Canvas Settings"),
          text_dyn(fn() { if settings_open.get() { "▼" } else { "▶" } }),
        ],
      ),
      show(fn() { settings_open.get() }, fn() { render_canvas_settings(state) }),
    ]),
    // Elements アコーディオン
    div(style="", [
      div(
        style=accordion_header_style,
        on=events().click(fn(_) { elements_open.update(fn(v) { not(v) }) }),
        [
          text_dyn(fn() {
            let count = state.elements
              .get()
              .filter(fn(el) { el.parent_id is None })
              .length()
            "Elements (\{count})"
          }),
          text_dyn(fn() { if elements_open.get() { "▼" } else { "▶" } }),
        ],
      ),
      show(fn() { elements_open.get() }, fn() { render_element_tree(state) }),
    ]),
  ])
}

///|
/// Canvas 設定パネル
fn render_canvas_settings(state : @core.EditorState) -> @element.DomNode {
  let row_style = "display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; font-size: 11px;"
  let label_style = "color: #666;"
  let text_input_style = "width: 70px; padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px; font-size: 10px;"
  let small_input_style = "width: 60px; padding: 2px 4px; border: 1px solid #ccc; border-radius: 3px; font-size: 10px;"
  let theme_btn_style = "flex: 1; padding: 6px 8px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; font-size: 11px; transition: all 0.2s;"
  div(
    style="padding: 8px; background: #fff; border: 1px solid #eee; border-radius: 4px;",
    [
      // Document Size
      div(style="margin-bottom: 12px;", [
        @element.span(
          style=label_style + " display: block; margin-bottom: 6px;",
          [text("Document Size")],
        ),
        div(style="display: flex; gap: 8px; align-items: center;", [
          @element.span(style="font-size: 10px; color: #999;", [text("W:")]),
          input(
            type_="number",
            style=small_input_style,
            dyn_value=fn() { state.doc_width.get().to_int().to_string() },
            on=events().change(fn(e) {
              let value = get_event_target_value(e.as_any())
              if parse_double(value) is Some(w) {
                if w > 0.0 {
                  state.doc_width.set(w)
                }
              }
            }),
          ),
          @element.span(style="font-size: 10px; color: #999;", [text("H:")]),
          input(
            type_="number",
            style=small_input_style,
            dyn_value=fn() { state.doc_height.get().to_int().to_string() },
            on=events().change(fn(e) {
              let value = get_event_target_value(e.as_any())
              if parse_double(value) is Some(h) {
                if h > 0.0 {
                  state.doc_height.set(h)
                }
              }
            }),
          ),
        ]),
        // Fit to Canvas ボタン
        button(
          style="margin-top: 6px; width: 100%; padding: 4px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; font-size: 10px; background: #f8f8f8;",
          on=events().click(fn(_) { state.fit_to_canvas() }),
          [text("Fit to Canvas")],
        ),
      ]),
      // テーマ切り替え
      div(
        style="margin-bottom: 12px; border-top: 1px solid #eee; padding-top: 8px;",
        [
          @element.span(
            style=label_style + " display: block; margin-bottom: 6px;",
            [text("Theme")],
          ),
          div(style="display: flex; gap: 4px;", [
            button(
              dyn_style=fn() {
                let is_active = state.theme_mode.get() == @model.Light
                if is_active {
                  theme_btn_style +
                  " background: #000; color: #fff; border-color: #000;"
                } else {
                  theme_btn_style + " background: #fff; color: #333;"
                }
              },
              on=events().click(fn(_) {
                state.theme_mode.set(@model.Light)
                let theme = @model.Theme::light()
                state.preview_bg.set(theme.background)
              }),
              [text("Light")],
            ),
            button(
              dyn_style=fn() {
                let is_active = state.theme_mode.get() == @model.Dark
                if is_active {
                  theme_btn_style +
                  " background: #000; color: #fff; border-color: #000;"
                } else {
                  theme_btn_style + " background: #fff; color: #333;"
                }
              },
              on=events().click(fn(_) {
                state.theme_mode.set(@model.Dark)
                let theme = @model.Theme::dark()
                state.preview_bg.set(theme.background)
              }),
              [text("Dark")],
            ),
          ]),
        ],
      ),
      // プリセットカラー
      div(
        style="margin-bottom: 12px; border-top: 1px solid #eee; padding-top: 8px;",
        [
          @element.span(
            style=label_style + " display: block; margin-bottom: 6px;",
            [text("Preset Colors")],
          ),
          render_preset_colors(state),
        ],
      ),
      // 埋め込み背景色
      div(style=row_style + " border-top: 1px solid #eee; padding-top: 8px;", [
        @element.span(style=label_style, [text("Embed BG")]),
        input(
          type_="text",
          style=text_input_style,
          placeholder="transparent",
          dyn_value=fn() {
            let bg = state.embed_bg.get()
            if bg == "transparent" {
              ""
            } else {
              bg
            }
          },
          on=events().input(fn(e) {
            let value = get_event_target_value(e.as_any())
            if value == "" {
              state.embed_bg.set("transparent")
            } else {
              state.embed_bg.set(value)
            }
          }),
        ),
      ]),
    ],
  )
}

///|
/// プリセットカラーを表示
fn render_preset_colors(state : @core.EditorState) -> @element.DomNode {
  let colors = @model.preset_colors()
  let children : Array[@element.DomNode] = []
  for preset in colors {
    let preset_name = preset.name
    let light_color = preset.light
    let dark_color = preset.dark
    children.push(
      div(
        style="display: flex; flex-direction: column; align-items: center; gap: 2px;",
        [
          div(
            dyn_style=fn() {
              let color = match state.theme_mode.get() {
                @model.Light => light_color
                @model.Dark => dark_color
              }
              "width: 24px; height: 24px; border-radius: 4px; border: 2px solid #ccc; cursor: pointer; background: \{color};"
            },
            on=events().click(fn(_) {
              // 選択中の要素があれば色を適用
              let color = match state.theme_mode.get() {
                @model.Light => light_color
                @model.Dark => dark_color
              }
              if state.get_selected_id() is Some(id) {
                state.update_element(id, fn(el) {
                  { ..el, style: { ..el.style, stroke: Some(color) } }
                })
              }
            }),
            [],
          ),
          @element.span(style="font-size: 9px; color: #999;", [
            text(preset_name),
          ]),
        ],
      ),
    )
  }
  div(style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;", [
    @element.fragment(children),
  ])
}

///|
/// 共通スタイルエディタコンポーネント(コンテキストメニュー・詳細パネル両方で使用)
/// close_menu: メニューを閉じるコールバック(コンテキストメニュー用、詳細パネルでは空の関数を渡す)
/// ShapeConfig を使用して、シェープごとに表示する項目を制御
pub fn render_style_editor(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  close_menu : () -> Unit,
) -> @element.DomNode {
  let theme = state.get_theme()
  let section_style = "margin-bottom: 8px;"
  let label_style = "font-size: 11px; color: \{theme.ui_text_muted}; min-width: 40px;"
  @element.fragment([
    // Stroke セクション(ShapeConfig.has_stroke で制御)
    show(
      fn() {
        match state.find_element(element_id) {
          Some(el) => el.shape.config().has_stroke
          None => false
        }
      },
      fn() {
        div(style=section_style, [
          div(style="display: flex; align-items: center; gap: 8px;", [
            @element.span(style=label_style, [text("Stroke")]),
            render_color_editor(state, history, element_id, false, close_menu),
          ]),
        ])
      },
    ),
    // Fill セクション(ShapeConfig.has_fill で制御)
    show(
      fn() {
        match state.find_element(element_id) {
          Some(el) => el.shape.config().has_fill
          None => false
        }
      },
      fn() {
        div(style=section_style, [
          div(style="display: flex; align-items: center; gap: 8px;", [
            @element.span(style=label_style, [text("Fill")]),
            render_color_editor(state, history, element_id, true, close_menu),
          ]),
        ])
      },
    ),
    // Text 専用オプション - フォント選択(has_font または子テキストがある場合)
    show(
      fn() {
        match state.find_element(element_id) {
          Some(el) =>
            // 自身がTextの場合、または子テキストがある場合に表示
            if el.shape.config().has_font {
              true
            } else {
              // 子要素にTextがあるかチェック
              let children = state.get_children(element_id)
              children
              .iter()
              .any(fn(child) { child.shape is @model.Text(_, _) })
            }
          None => false
        }
      },
      fn() { render_text_style_editor(state, history, element_id, close_menu) },
    ),
    // Line/Path 専用オプション - 破線・矢印(ShapeConfig.has_dasharray/has_arrows で制御)
    show(
      fn() {
        match state.find_element(element_id) {
          Some(el) => {
            let config = el.shape.config()
            config.has_dasharray || config.has_arrows
          }
          None => false
        }
      },
      fn() { render_line_style_editor(state, history, element_id, close_menu) },
    ),
  ])
}

///|
/// 色エディタ(カラーパレット統合版)
fn render_color_editor(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  is_fill : Bool,
  _close_menu : () -> Unit,
) -> @element.DomNode {
  // カラーパレット(基本色 + transparent)
  let base_colors = [
    "#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00",
  ]
  div(style="display: flex; align-items: center; gap: 3px;", [
    // 透明ボタン(is_fill の場合のみ)
    if is_fill {
      render_color_button(state, history, element_id, is_fill, "transparent")
    } else {
      @element.fragment([])
    },
    // カラーパレット
    @element.fragment(
      base_colors.map(fn(color) {
        render_color_button(state, history, element_id, is_fill, color)
      }),
    ),
    // ネイティブカラーピッカー
    input(
      type_="color",
      style="width: 22px; height: 22px; padding: 0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;",
      dyn_value=fn() {
        match state.find_element(element_id) {
          Some(el) => {
            let color = if is_fill {
              el.style.fill.unwrap_or("transparent")
            } else {
              el.style.stroke.unwrap_or("#000000")
            }
            to_hex_color(color, if is_fill { "#ffffff" } else { "#000000" })
          }
          None => if is_fill { "#ffffff" } else { "#000000" }
        }
      },
      on=events().input(fn(e) {
        if state.find_element(element_id) is Some(el) {
          let value = get_event_target_value(e.as_any())
          let old_style = el.style
          let new_style = if is_fill {
            { ..old_style, fill: Some(value) }
          } else {
            { ..old_style, stroke: Some(value) }
          }
          @core.execute_command(
            history,
            state,
            @model.UpdateStyle(element_id, old_style, new_style),
          )
        }
      }),
    ),
  ])
}

///|
/// カラーボタン(選択状態付き)
fn render_color_button(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  is_fill : Bool,
  color : String,
) -> @element.DomNode {
  @element.create_element(
    "button",
    [
      (
        "style",
        @element.Dynamic(fn() {
          let current_color = match state.find_element(element_id) {
            Some(el) =>
              if is_fill {
                el.style.fill.unwrap_or("transparent")
              } else {
                el.style.stroke.unwrap_or("#000000")
              }
            None => if is_fill { "transparent" } else { "#000000" }
          }
          let is_selected = current_color == color ||
            (color == "transparent" && current_color == "transparent")
          let border = if is_selected {
            "2px solid #0066ff"
          } else {
            "1px solid #ccc"
          }
          let bg = if color == "transparent" {
            "linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%); background-size: 6px 6px; background-position: 0 0, 0 3px, 3px -3px, -3px 0px"
          } else {
            color
          }
          "width: 22px; height: 22px; padding: 0; border: \{border}; border-radius: 3px; cursor: pointer; background: \{bg};"
        }),
      ),
      ("title", @element.Static(color)),
      (
        "click",
        @element.Handler(fn(_) {
          if state.find_element(element_id) is Some(el) {
            let old_style = el.style
            let new_value : String? = if color == "transparent" {
              None
            } else {
              Some(color)
            }
            let new_style = if is_fill {
              { ..old_style, fill: new_value }
            } else {
              { ..old_style, stroke: new_value }
            }
            @core.execute_command(
              history,
              state,
              @model.UpdateStyle(element_id, old_style, new_style),
            )
          }
        }),
      ),
    ],
    [],
  )
}

///|
/// Text 用スタイルエディタ(フォント選択)
fn render_text_style_editor(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  _close_menu : () -> Unit,
) -> @element.DomNode {
  let theme = state.get_theme()
  div(
    style="margin-top: 6px; padding-top: 6px; border-top: 1px solid \{theme.ui_border}; display: flex; align-items: center; gap: 6px;",
    [
      @element.span(style="font-size: 11px; color: \{theme.ui_text_muted};", [
        text("Font"),
      ]),
      div(style="display: flex; gap: 2px;", [
        render_font_buttons_shared(state, history, element_id),
      ]),
    ],
  )
}

///|
/// フォント変更対象のテキスト要素IDを取得
/// - 選択要素がTextの場合: その要素のID
/// - 選択要素に子Textがある場合: 最初の子TextのID
/// - それ以外: None
fn get_font_target_id(state : @core.EditorState) -> String? {
  guard state.get_selected_id() is Some(id) else { return None }
  guard state.find_element(id) is Some(el) else { return None }

  // 自身がTextの場合
  if el.shape is @model.Text(_, _) {
    return Some(id)
  }

  // 子要素にTextがあるかチェック
  let children = state.get_children(id)
  for child in children {
    if child.shape is @model.Text(_, _) {
      return Some(child.id)
    }
  }
  None
}

///|
/// 共通フォントボタン群
/// Note: 選択要素がTextの場合は直接、親要素の場合は子Textのフォントを変更
fn render_font_buttons_shared(
  state : @core.EditorState,
  history : @model.History,
  _element_id : String,
) -> @element.DomNode {
  let buttons : Array[@element.DomNode] = []
  for preset in font_family_presets {
    let (label, font_value) = preset
    buttons.push(
      button(
        style=toggle_button_style,
        dyn_style=fn() {
          // フォント変更対象のテキスト要素を取得
          let is_selected = match get_font_target_id(state) {
            Some(target_id) =>
              match state.find_element(target_id) {
                Some(el) => el.style.font_family == font_value
                None => false
              }
            None => false
          }
          if is_selected {
            toggle_button_style + " background: #e3f2fd; border-color: #2196f3;"
          } else {
            toggle_button_style + " background: white;"
          }
        },
        on=events().click(fn(_) {
          // フォント変更対象のテキスト要素を取得してスタイルを更新
          if get_font_target_id(state) is Some(target_id) {
            if state.find_element(target_id) is Some(el) {
              let old_style = el.style
              let new_style = { ..old_style, font_family: font_value }
              @core.execute_command(
                history,
                state,
                @model.UpdateStyle(target_id, old_style, new_style),
              )
            }
          }
        }),
        [text(label)],
      ),
    )
  }
  @element.fragment(buttons)
}

///|
/// Line 用スタイルエディタ(破線、矢印)
fn render_line_style_editor(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  _close_menu : () -> Unit,
) -> @element.DomNode {
  let theme = state.get_theme()
  div(
    style="margin-top: 6px; padding-top: 6px; border-top: 1px solid \{theme.ui_border}; display: flex; align-items: center; gap: 6px; flex-wrap: wrap;",
    [
      // 破線パターン選択
      div(style="display: flex; gap: 2px;", [
        render_dasharray_buttons_shared(state, history, element_id),
      ]),
      // セパレータ
      @element.span(style="color: #ccc;", [text("|")]),
      // Arrow トグルボタン
      div(style="display: flex; gap: 4px; align-items: center;", [
        // Start Arrow
        button(
          style=toggle_button_style,
          dyn_style=fn() {
            let is_selected = match state.find_element(element_id) {
              Some(el) => el.style.marker_start is Some(@model.Arrow)
              None => false
            }
            if is_selected {
              toggle_button_style +
              " background: #e3f2fd; border-color: #2196f3;"
            } else {
              toggle_button_style + " background: white;"
            }
          },
          on=events().click(fn(_) {
            if state.find_element(element_id) is Some(el) {
              let old_style = el.style
              let new_marker = if old_style.marker_start is Some(@model.Arrow) {
                None
              } else {
                Some(@model.Arrow)
              }
              let new_style = { ..old_style, marker_start: new_marker }
              @core.execute_command(
                history,
                state,
                @model.UpdateStyle(element_id, old_style, new_style),
              )
            }
          }),
          [text("<")],
        ),
        // End Arrow
        button(
          style=toggle_button_style,
          dyn_style=fn() {
            let is_selected = match state.find_element(element_id) {
              Some(el) => el.style.marker_end is Some(@model.Arrow)
              None => false
            }
            if is_selected {
              toggle_button_style +
              " background: #e3f2fd; border-color: #2196f3;"
            } else {
              toggle_button_style + " background: white;"
            }
          },
          on=events().click(fn(_) {
            if state.find_element(element_id) is Some(el) {
              let old_style = el.style
              let new_marker = if old_style.marker_end is Some(@model.Arrow) {
                None
              } else {
                Some(@model.Arrow)
              }
              let new_style = { ..old_style, marker_end: new_marker }
              @core.execute_command(
                history,
                state,
                @model.UpdateStyle(element_id, old_style, new_style),
              )
            }
          }),
          [text(">")],
        ),
      ]),
    ],
  )
}

///|
/// 共通破線パターンボタン群
fn render_dasharray_buttons_shared(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
) -> @element.DomNode {
  let children : Array[@element.DomNode] = []
  for preset in dasharray_presets {
    let (label, pattern) = preset
    let btn = button(
      style=toggle_button_style,
      dyn_style=fn() {
        match state.find_element(element_id) {
          Some(el) => {
            let current = el.style.stroke_dasharray
            let is_active = match (current, pattern) {
              (None, None) => true
              (Some(a), Some(b)) => a == b
              _ => false
            }
            if is_active {
              toggle_button_style +
              " background: #e0e0ff; border-color: #6666ff;"
            } else {
              toggle_button_style + " background: #fff;"
            }
          }
          None => toggle_button_style + " background: #fff;"
        }
      },
      on=events().click(fn(_) {
        if state.find_element(element_id) is Some(el) {
          let old_style = el.style
          let new_style = { ..old_style, stroke_dasharray: pattern }
          @core.execute_command(
            history,
            state,
            @model.UpdateStyle(element_id, old_style, new_style),
          )
        }
      }),
      [text(label)],
    )
    children.push(btn)
  }
  @element.fragment(children)
}

///|
/// Layer 操作コンポーネント
pub fn render_layer_operations(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  close_menu : () -> Unit,
) -> @element.DomNode {
  let theme = state.get_theme()
  let btn_style = menu_button_style + " color: \{theme.ui_text};"
  let section_style = "padding: 4px 0;"
  @element.fragment([
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          if state.bring_to_front(element_id) is Some((from_idx, to_idx)) {
            let cmd = @model.ReorderElement(element_id, from_idx, to_idx)
            history.undo_stack.push(cmd)
            history.redo_stack.clear()
          }
          close_menu()
        }),
        [text("Bring to Front")],
      ),
    ]),
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          if state.bring_forward(element_id) is Some((from_idx, to_idx)) {
            let cmd = @model.ReorderElement(element_id, from_idx, to_idx)
            history.undo_stack.push(cmd)
            history.redo_stack.clear()
          }
          close_menu()
        }),
        [text("Bring Forward")],
      ),
    ]),
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          if state.send_backward(element_id) is Some((from_idx, to_idx)) {
            let cmd = @model.ReorderElement(element_id, from_idx, to_idx)
            history.undo_stack.push(cmd)
            history.redo_stack.clear()
          }
          close_menu()
        }),
        [text("Send Backward")],
      ),
    ]),
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          if state.send_to_back(element_id) is Some((from_idx, to_idx)) {
            let cmd = @model.ReorderElement(element_id, from_idx, to_idx)
            history.undo_stack.push(cmd)
            history.redo_stack.clear()
          }
          close_menu()
        }),
        [text("Send to Back")],
      ),
    ]),
  ])
}

///|
/// 削除ボタンコンポーネント
pub fn render_delete_button(
  state : @core.EditorState,
  history : @model.History,
  element_id : String,
  close_menu : () -> Unit,
) -> @element.DomNode {
  let btn_style = menu_button_style + " color: #e53e3e;"
  button(
    style=btn_style,
    on=events().click(fn(_) {
      if state.find_element(element_id) is Some(el) {
        @core.execute_command(history, state, @model.RemoveElement(el))
      }
      close_menu()
    }),
    [text("Delete")],
  )
}

///|
/// 要素ツリーをレンダリング
fn render_element_tree(state : @core.EditorState) -> @element.DomNode {
  let tree_style = "max-height: 300px; overflow-y: auto; padding: 4px;"
  let item_style = "display: flex; align-items: center; gap: 4px; padding: 4px 8px; cursor: pointer; border-radius: 3px; font-size: 11px;"
  // 親要素のみを表示(子要素は除外)
  let elements = state.elements.get().filter(fn(el) { el.parent_id is None })
  let children : Array[@element.DomNode] = []
  for el in elements {
    let shape_icon = match el.shape {
      @model.Rect(_, _, _, _) => "▢"
      @model.Circle(_) => "○"
      @model.Ellipse(_, _) => "⬭"
      @model.Line(_, _) => "╱"
      @model.Polyline(_) => "⟋"
      @model.Path(_, _, _, _, _) => "✎"
      @model.Text(_, _) => "T"
    }
    let el_id = el.id
    let shape_name = @model.shape_name(el.shape)
    children.push(
      div(
        style=item_style,
        on=events().click(fn(_) { state.select(Some(el_id)) }),
        [
          @element.span(style="font-size: 14px; width: 16px;", [
            text(shape_icon),
          ]),
          @element.span(style="color: #666;", [text(shape_name)]),
          @element.span(style="color: #999; font-family: monospace;", [
            text("#" + el_id),
          ]),
        ],
      ),
    )
  }
  if children.length() == 0 {
    div(
      style="padding: 8px; color: #999; font-size: 11px; text-align: center;",
      [text("No elements")],
    )
  } else {
    div(style=tree_style, [@element.fragment(children)])
  }
}

///|
/// 小さい入力フィールドのスタイル(コンパクト版)
let small_input_style : String = "width: 48px; padding: 2px 4px; border: 1px solid #ddd; border-radius: 3px; font-family: monospace; font-size: 11px; box-sizing: border-box;"

///|
/// 読み取り専用フィールドのスタイル
let readonly_field_style : String = "font-family: monospace; font-size: 11px; padding: 2px 4px; background: #f5f5f5; border-radius: 3px; min-width: 36px; text-align: center;"

///|
/// ID入力フィールドのスタイル
let id_input_style : String = "flex: 1; padding: 2px 6px; border: 1px solid #ddd; border-radius: 3px; font-family: monospace; font-size: 11px; box-sizing: border-box;"

///|
/// ラベルのスタイル
let label_style : String = "font-size: 11px; color: #888; min-width: 12px;"

///|
/// 行のスタイル
let row_style : String = "display: flex; align-items: center; gap: 6px; margin-bottom: 6px;"

///|
/// 要素の詳細を表示・編集(コンパクト版)
fn render_element_details(
  state : @core.EditorState,
  history : @model.History,
) -> @element.DomNode {
  let id_error = @luna.signal(false)
  div(style="font-size: 12px;", [
    // Shape type + ID (一行)
    div(style=row_style, [
      @element.span(style="font-size: 12px; color: #333; font-weight: 500;", [
        text_dyn(fn() {
          match state.get_selected_id() {
            Some(id) =>
              match state.find_element(id) {
                Some(el) => @model.shape_name(el.shape)
                None => "-"
              }
            None => "-"
          }
        }),
      ]),
      @element.span(style="font-size: 11px; color: #999;", [text("#")]),
      input(
        type_="text",
        style=id_input_style,
        dyn_style=fn() {
          if id_error.get() {
            id_input_style + " border-color: #e53e3e; background: #fff5f5;"
          } else {
            id_input_style
          }
        },
        dyn_value=fn() { state.get_selected_id().unwrap_or("") },
        on=events().change(fn(e) {
          if state.get_selected_id() is Some(old_id) {
            let new_id = get_event_target_value(e.as_any())
            if new_id != "" && new_id != old_id {
              if state.has_element_with_id(new_id) {
                id_error.set(true)
              } else {
                @core.execute_command(
                  history,
                  state,
                  @model.RenameElement(old_id, new_id),
                )
                id_error.set(false)
              }
            }
          }
        }),
      ),
    ]),
    // Position & Size (2行をコンパクトに)
    div(
      style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; margin-bottom: 8px; padding: 6px; background: #f8f8f8; border-radius: 4px;",
      [
        // x
        div(style="display: flex; align-items: center; gap: 4px;", [
          @element.span(style=label_style, [text("x")]),
          input(
            type_="number",
            style=small_input_style + " flex: 1;",
            dyn_value=fn() {
              match state.get_selected_id() {
                Some(id) =>
                  match state.find_element(id) {
                    Some(el) => el.x.to_int().to_string()
                    None => "0"
                  }
                None => "0"
              }
            },
            on=events().input(fn(e) {
              if state.get_selected_id() is Some(id) {
                if state.find_element(id) is Some(el) {
                  let value = get_event_target_value(e.as_any())
                  if parse_double(value) is Some(new_x) {
                    @core.execute_command(
                      history,
                      state,
                      @model.MoveElement(id, el.x, el.y, new_x, el.y),
                    )
                  }
                }
              }
            }),
          ),
        ]),
        // y
        div(style="display: flex; align-items: center; gap: 4px;", [
          @element.span(style=label_style, [text("y")]),
          input(
            type_="number",
            style=small_input_style + " flex: 1;",
            dyn_value=fn() {
              match state.get_selected_id() {
                Some(id) =>
                  match state.find_element(id) {
                    Some(el) => el.y.to_int().to_string()
                    None => "0"
                  }
                None => "0"
              }
            },
            on=events().input(fn(e) {
              if state.get_selected_id() is Some(id) {
                if state.find_element(id) is Some(el) {
                  let value = get_event_target_value(e.as_any())
                  if parse_double(value) is Some(new_y) {
                    @core.execute_command(
                      history,
                      state,
                      @model.MoveElement(id, el.x, el.y, el.x, new_y),
                    )
                  }
                }
              }
            }),
          ),
        ]),
        // w (readonly)
        div(style="display: flex; align-items: center; gap: 4px;", [
          @element.span(style=label_style, [text("w")]),
          @element.span(style=readonly_field_style + " flex: 1;", [
            text_dyn(fn() {
              match state.get_selected_id() {
                Some(id) =>
                  match state.find_element(id) {
                    Some(el) => el.bounding_box().width.to_int().to_string()
                    None => "-"
                  }
                None => "-"
              }
            }),
          ]),
        ]),
        // h (readonly)
        div(style="display: flex; align-items: center; gap: 4px;", [
          @element.span(style=label_style, [text("h")]),
          @element.span(style=readonly_field_style + " flex: 1;", [
            text_dyn(fn() {
              match state.get_selected_id() {
                Some(id) =>
                  match state.find_element(id) {
                    Some(el) => el.bounding_box().height.to_int().to_string()
                    None => "-"
                  }
                None => "-"
              }
            }),
          ]),
        ]),
      ],
    ),
    // Style section - 共通コンポーネント使用(Stroke, Fill, Text font, Line style)
    show(fn() { state.get_selected_id() is Some(_) }, fn() {
      let id = state.get_selected_id().unwrap()
      let noop = fn() {  }
      @element.fragment([
        render_style_editor(state, history, id, noop),
        // stroke-width(詳細パネル専用、ShapeConfig.has_stroke_width で制御)
        show(
          fn() {
            match state.find_element(id) {
              Some(el) => el.shape.config().has_stroke_width
              None => false
            }
          },
          fn() {
            div(
              style="display: flex; align-items: center; gap: 8px; margin-top: 8px;",
              [
                @element.span(
                  style="font-size: 11px; color: #666; min-width: 40px;",
                  [text("Width")],
                ),
                input(
                  type_="number",
                  style="width: 50px; padding: 2px 4px; border: 1px solid #ddd; border-radius: 3px; font-size: 10px;",
                  attrs=[
                    ("min", @element.AttrString("0")),
                    ("step", @element.AttrString("0.5")),
                  ],
                  dyn_value=fn() {
                    match state.find_element(id) {
                      Some(el) =>
                        el.style.stroke_width.unwrap_or(1.0).to_string()
                      None => "1"
                    }
                  },
                  on=events().change(fn(e) {
                    if state.find_element(id) is Some(el) {
                      let value = get_event_target_value(e.as_any())
                      if parse_double(value) is Some(w) {
                        let old_style = el.style
                        let new_style = { ..old_style, stroke_width: Some(w) }
                        @core.execute_command(
                          history,
                          state,
                          @model.UpdateStyle(id, old_style, new_style),
                        )
                      }
                    }
                  }),
                ),
                @element.span(style="font-size: 10px; color: #999;", [
                  text("px"),
                ]),
              ],
            )
          },
        ),
      ])
    }),
  ])
}

///|
/// トグルボタンのスタイル
let toggle_button_style : String = "padding: 2px 6px; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; font-size: 10px;"

///|
/// フォントファミリーのプリセット
let font_family_presets : Array[(String, String?)] = [
  ("Sans", None), // システムデフォルト
  ("Serif", Some("serif")),
  ("Mono", Some("monospace")),
]

///|
/// 破線パターンのプリセット(短いラベル)
let dasharray_presets : Array[(String, String?)] = [
  ("―", None),
  ("--", Some("8,4")),
  ("··", Some("2,4")),
  ("-·", Some("8,4,2,4")),
]

///|
/// テキスト入力オーバーレイをレンダリング
pub fn render_text_input(
  state : @core.EditorState,
  commit : (String) -> Unit,
  cancel : () -> Unit,
) -> @element.DomNode {
  guard state.text_edit.get() is Some(edit_state)
  let vp = state.viewport.get()
  // 親要素がある場合は親の中央座標を使用
  let (text_x, text_y, max_width) : (Double, Double, Double) = match
    state.find_element(edit_state.parent_id) {
    Some(parent) => {
      let bbox = parent.bounding_box()
      let center_x = bbox.x + bbox.width / 2.0
      let center_y = bbox.y + bbox.height / 2.0
      (center_x, center_y, bbox.width * vp.zoom - 16.0)
    }
    None => (edit_state.x, edit_state.y, 200.0)
  }
  // シーン座標をスクリーン座標に変換
  let screen_pos = vp.scene_to_screen(text_x, text_y)
  // テキストエリア(背景色でSVGテキストを覆う)
  let min_width = if max_width < 40.0 { 40.0 } else { max_width }
  let style = "position: absolute; left: \{screen_pos.x}px; top: \{screen_pos.y}px; transform: translate(-50%, -50%); min-width: \{min_width}px; height: 24px; padding: 2px 4px; border: none; background: #ffffff; color: #000000; font-family: sans-serif; font-size: 16px; line-height: 1.2; text-align: center; outline: none; box-sizing: border-box; resize: none; overflow: hidden; caret-color: currentColor; white-space: nowrap; border-radius: 2px;"
  // リアルタイム更新用のコールバック
  let editing_id = edit_state.editing_id
  let initial_text = edit_state.initial_text
  let on_input = fn(value : String) {
    match editing_id {
      Some(id) => state.update_text_raw(id, value)
      None => ()
    }
  }
  let on_escape = fn(_value : String) {
    // キャンセル時は元のテキストに戻す
    match editing_id {
      Some(id) => state.update_text_raw(id, initial_text)
      None => ()
    }
    cancel()
  }
  // FFIでtextareaを作成
  let textarea = create_textarea_ffi(
    style,
    initial_text,
    fn(value) { commit(value) },
    on_escape,
    on_input,
  )
  let node : @js_dom.Node = textarea.cast()
  @element.dom_node(node)
}

///|
/// コンテキストメニューをレンダリング
pub fn render_context_menu(
  state : @core.EditorState,
  history : @model.History,
  new_id : () -> String,
) -> @element.DomNode {
  guard state.context_menu.get() is Some(menu)
  let theme = state.get_theme()
  let is_dark = state.theme_mode.get() == @model.Dark
  let shadow = if is_dark { "rgba(0,0,0,0.4)" } else { "rgba(0,0,0,0.1)" }
  let style = "position: fixed; left: \{menu.x}px; top: \{menu.y}px; background: \{theme.ui_bg}; border: 1px solid \{theme.ui_border}; border-radius: 4px; box-shadow: 2px 2px 10px \{shadow}; min-width: 140px; z-index: 1000; color: \{theme.ui_text};"
  let section_style = "padding: 4px 12px; border-bottom: 1px solid \{theme.ui_border};"
  let header_style = "padding: 4px 12px; border-bottom: 1px solid \{theme.ui_border}; color: \{theme.ui_text_muted}; font-size: 11px;"
  let close_menu = fn() { state.hide_context_menu() }
  div(style~, [
    // 要素上でクリックした場合のみメニューを表示
    if menu.target_id is Some(id) {
      @element.fragment([
        // Style section - 共通コンポーネント使用
        div(style=section_style, [
          render_style_editor(state, history, id, close_menu),
        ]),
        // Layer section header
        div(style=header_style, [text("Layer")]),
        // Layer operations - 共通コンポーネント使用
        render_layer_operations(state, history, id, close_menu),
        // Delete - 共通コンポーネント使用
        div(style="padding: 4px 12px;", [
          render_delete_button(state, history, id, close_menu),
        ]),
      ])
    } else {
      // 空白部分を右クリックした場合 - 挿入メニュー
      render_insert_menu(state, history, new_id, menu.scene_x, menu.scene_y)
    },
  ])
}

///|
/// 挿入メニューをレンダリング
fn render_insert_menu(
  state : @core.EditorState,
  history : @model.History,
  new_id : () -> String,
  insert_x : Double,
  insert_y : Double,
) -> @element.DomNode {
  let theme = state.get_theme()
  let section_style = "padding: 4px 12px; border-bottom: 1px solid \{theme.ui_border};"
  let header_style = "padding: 4px 12px; border-bottom: 1px solid \{theme.ui_border}; color: \{theme.ui_text_muted}; font-size: 12px;"
  let btn_style = menu_button_style + " color: \{theme.ui_text};"
  @element.fragment([
    div(style=header_style, [text("Insert")]),
    // Rectangle
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          let el = @model.Element::new(
            new_id(),
            insert_x,
            insert_y,
            @model.Rect(80.0, 60.0, None, None),
          ).with_style(state.get_default_style())
          @core.execute_command(history, state, @model.AddElement(el))
          state.hide_context_menu()
        }),
        [text("Rectangle")],
      ),
    ]),
    // Circle
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          let el = @model.Element::new(
            new_id(),
            insert_x,
            insert_y,
            @model.Circle(40.0),
          ).with_style(state.get_default_style())
          @core.execute_command(history, state, @model.AddElement(el))
          state.hide_context_menu()
        }),
        [text("Circle")],
      ),
    ]),
    // Ellipse
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          let el = @model.Element::new(
            new_id(),
            insert_x,
            insert_y,
            @model.Ellipse(60.0, 35.0),
          ).with_style(state.get_default_style())
          @core.execute_command(history, state, @model.AddElement(el))
          state.hide_context_menu()
        }),
        [text("Ellipse")],
      ),
    ]),
    // Line
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          let el = @model.Element::new(
            new_id(),
            insert_x,
            insert_y,
            @model.Line(insert_x + 100.0, insert_y + 50.0),
          ).with_style(state.get_line_style())
          @core.execute_command(history, state, @model.AddElement(el))
          state.hide_context_menu()
        }),
        [text("Line")],
      ),
    ]),
    // Arrow
    div(style=section_style, [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          let el = @model.Element::new(
            new_id(),
            insert_x,
            insert_y,
            @model.Line(insert_x + 100.0, insert_y + 50.0),
          ).with_style(state.get_arrow_style())
          @core.execute_command(history, state, @model.AddElement(el))
          state.hide_context_menu()
        }),
        [text("Arrow")],
      ),
    ]),
    // Text
    div(style="padding: 4px 12px;", [
      button(
        style=btn_style,
        on=events().click(fn(_) {
          let el = @model.Element::new(
            new_id(),
            insert_x,
            insert_y,
            @model.Text("Text", Some(24.0)),
          ).with_style(state.get_text_style())
          @core.execute_command(history, state, @model.AddElement(el))
          state.hide_context_menu()
        }),
        [text("Text")],
      ),
    ]),
  ])
}