Spaces:
Runtime error
Runtime error
Allow hand-drawn links between neighboring plants on the garden board
Browse filesAdd a "Relier des plantes" link mode: click two sprites to connect
(or disconnect) them. Linked pairs are stored bidirectionally as
neighbors, drawn as dashed lines on the board, and surfaced to the
gardening advisor so it can factor in companion-planting effects.
- app.py +101 -5
- modules/advisor.py +13 -3
- static/style.css +35 -0
app.py
CHANGED
|
@@ -52,6 +52,24 @@ LON = 5.3698
|
|
| 52 |
BOARD_JS = """
|
| 53 |
<script>
|
| 54 |
(function () {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
function attachBoard() {
|
| 56 |
const board = document.getElementById('garden-board');
|
| 57 |
if (!board || board._wired) return;
|
|
@@ -107,6 +125,21 @@ BOARD_JS = """
|
|
| 107 |
window._gardenMove = { idx: drag.idx, x: x, y: y };
|
| 108 |
const btn = document.getElementById('garden-move-btn');
|
| 109 |
if (btn) btn.click();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
} else {
|
| 111 |
window._gardenSelect = { idx: drag.idx };
|
| 112 |
const btn = document.getElementById('garden-select-btn');
|
|
@@ -118,10 +151,15 @@ BOARD_JS = """
|
|
| 118 |
board.addEventListener('pointercancel', () => { drag = null; });
|
| 119 |
}
|
| 120 |
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
observer.observe(document.body, { childList: true, subtree: true });
|
| 123 |
-
document.addEventListener('DOMContentLoaded',
|
| 124 |
-
|
| 125 |
})();
|
| 126 |
</script>
|
| 127 |
"""
|
|
@@ -293,6 +331,7 @@ def get_garden_board_html(user_id: str) -> str:
|
|
| 293 |
garden = load_garden(user_id)
|
| 294 |
today = datetime.date.today()
|
| 295 |
tiles = []
|
|
|
|
| 296 |
for idx, p in enumerate(garden):
|
| 297 |
sprite_path = Path(pixel_art.get_sprite_path(p["genus"])).resolve()
|
| 298 |
sprite_url = f"/gradio_api/file={sprite_path.as_posix()}"
|
|
@@ -310,13 +349,31 @@ def get_garden_board_html(user_id: str) -> str:
|
|
| 310 |
caption += " 💧"
|
| 311 |
|
| 312 |
pos = p.get("position") or _default_position(idx)
|
|
|
|
| 313 |
tiles.append(
|
| 314 |
f'<div class="garden-sprite" data-idx="{idx}" style="left:{pos["x"]}%; top:{pos["y"]}%;">'
|
| 315 |
f'<img src="{sprite_url}" draggable="false" alt="{html.escape(caption)}">'
|
| 316 |
f'<div class="garden-caption">{html.escape(caption)}</div>'
|
| 317 |
f'</div>'
|
| 318 |
)
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
def _visible_plants(user_id: str) -> list[dict]:
|
| 322 |
"""All garden plants, in the same order as the gallery."""
|
|
@@ -500,7 +557,9 @@ with gr.Blocks(title="🌿 Plant Watering Planner") as app:
|
|
| 500 |
with gr.Row():
|
| 501 |
|
| 502 |
with gr.Column(scale=3):
|
| 503 |
-
gr.
|
|
|
|
|
|
|
| 504 |
|
| 505 |
# Garden board — freely draggable pixel-art sprites
|
| 506 |
garden_board = gr.HTML(value="", elem_id="garden-board")
|
|
@@ -513,6 +572,9 @@ with gr.Blocks(title="🌿 Plant Watering Planner") as app:
|
|
| 513 |
board_move_x = gr.Number(value=0, visible=False)
|
| 514 |
board_move_y = gr.Number(value=0, visible=False)
|
| 515 |
board_move_btn = gr.Button("sync", elem_id="garden-move-btn", elem_classes=["board-sync"])
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
# Detail panel — appears on click
|
| 518 |
plant_detail = gr.Markdown("_Click a plant photo to see its details._", elem_id="plant-detail")
|
|
@@ -606,6 +668,30 @@ with gr.Blocks(title="🌿 Plant Watering Planner") as app:
|
|
| 606 |
js="(idx, x, y, user_id) => { const d = window._gardenMove || {idx: -1, x: 0, y: 0}; return [d.idx, d.x, d.y, user_id]; }",
|
| 607 |
)
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
def _mark_watered_by_idx(idx, user_id):
|
| 610 |
if idx is None:
|
| 611 |
return "⚠️ Select a plant first.", get_garden_board_html(user_id)
|
|
@@ -631,6 +717,9 @@ with gr.Blocks(title="🌿 Plant Watering Planner") as app:
|
|
| 631 |
return "⚠️ Plant not found.", get_garden_board_html(user_id)
|
| 632 |
target_id = visible[idx]["id"]
|
| 633 |
garden = [p for p in load_garden(user_id) if p.get("id") != target_id]
|
|
|
|
|
|
|
|
|
|
| 634 |
save_garden(garden, user_id)
|
| 635 |
return "🗑️ Plant removed.", get_garden_board_html(user_id)
|
| 636 |
|
|
@@ -657,11 +746,18 @@ with gr.Blocks(title="🌿 Plant Watering Planner") as app:
|
|
| 657 |
return "Select a plant first."
|
| 658 |
plant = plants[idx]
|
| 659 |
info = get_plant_info(plant["genus"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
return advisor.ask_about_plant(
|
| 661 |
question, info,
|
| 662 |
plant_name=plant.get("nickname"),
|
| 663 |
genus=plant["genus"],
|
| 664 |
last_watered=plant.get("last_watered"),
|
|
|
|
| 665 |
)
|
| 666 |
|
| 667 |
advisor_ask_btn.click(
|
|
|
|
| 52 |
BOARD_JS = """
|
| 53 |
<script>
|
| 54 |
(function () {
|
| 55 |
+
function attachLinkModeBtn() {
|
| 56 |
+
const linkBtn = document.getElementById('link-mode-btn');
|
| 57 |
+
const board = document.getElementById('garden-board');
|
| 58 |
+
if (!linkBtn || linkBtn._wired) return;
|
| 59 |
+
linkBtn._wired = true;
|
| 60 |
+
|
| 61 |
+
linkBtn.addEventListener('click', () => {
|
| 62 |
+
window._linkMode = !window._linkMode;
|
| 63 |
+
linkBtn.classList.toggle('link-mode-active', window._linkMode);
|
| 64 |
+
if (board) board.classList.toggle('link-mode', window._linkMode);
|
| 65 |
+
if (window._linkSource != null && board) {
|
| 66 |
+
const srcEl = board.querySelector(`.garden-sprite[data-idx="${window._linkSource}"]`);
|
| 67 |
+
if (srcEl) srcEl.classList.remove('link-source');
|
| 68 |
+
}
|
| 69 |
+
window._linkSource = null;
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
function attachBoard() {
|
| 74 |
const board = document.getElementById('garden-board');
|
| 75 |
if (!board || board._wired) return;
|
|
|
|
| 125 |
window._gardenMove = { idx: drag.idx, x: x, y: y };
|
| 126 |
const btn = document.getElementById('garden-move-btn');
|
| 127 |
if (btn) btn.click();
|
| 128 |
+
} else if (window._linkMode) {
|
| 129 |
+
if (window._linkSource == null) {
|
| 130 |
+
window._linkSource = drag.idx;
|
| 131 |
+
drag.el.classList.add('link-source');
|
| 132 |
+
} else if (window._linkSource === drag.idx) {
|
| 133 |
+
drag.el.classList.remove('link-source');
|
| 134 |
+
window._linkSource = null;
|
| 135 |
+
} else {
|
| 136 |
+
const srcEl = board.querySelector(`.garden-sprite[data-idx="${window._linkSource}"]`);
|
| 137 |
+
if (srcEl) srcEl.classList.remove('link-source');
|
| 138 |
+
window._gardenLink = { idx1: window._linkSource, idx2: drag.idx };
|
| 139 |
+
window._linkSource = null;
|
| 140 |
+
const btn = document.getElementById('garden-link-btn');
|
| 141 |
+
if (btn) btn.click();
|
| 142 |
+
}
|
| 143 |
} else {
|
| 144 |
window._gardenSelect = { idx: drag.idx };
|
| 145 |
const btn = document.getElementById('garden-select-btn');
|
|
|
|
| 151 |
board.addEventListener('pointercancel', () => { drag = null; });
|
| 152 |
}
|
| 153 |
|
| 154 |
+
function attachAll() {
|
| 155 |
+
attachBoard();
|
| 156 |
+
attachLinkModeBtn();
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
const observer = new MutationObserver(attachAll);
|
| 160 |
observer.observe(document.body, { childList: true, subtree: true });
|
| 161 |
+
document.addEventListener('DOMContentLoaded', attachAll);
|
| 162 |
+
attachAll();
|
| 163 |
})();
|
| 164 |
</script>
|
| 165 |
"""
|
|
|
|
| 331 |
garden = load_garden(user_id)
|
| 332 |
today = datetime.date.today()
|
| 333 |
tiles = []
|
| 334 |
+
positions = {}
|
| 335 |
for idx, p in enumerate(garden):
|
| 336 |
sprite_path = Path(pixel_art.get_sprite_path(p["genus"])).resolve()
|
| 337 |
sprite_url = f"/gradio_api/file={sprite_path.as_posix()}"
|
|
|
|
| 349 |
caption += " 💧"
|
| 350 |
|
| 351 |
pos = p.get("position") or _default_position(idx)
|
| 352 |
+
positions[p["id"]] = pos
|
| 353 |
tiles.append(
|
| 354 |
f'<div class="garden-sprite" data-idx="{idx}" style="left:{pos["x"]}%; top:{pos["y"]}%;">'
|
| 355 |
f'<img src="{sprite_url}" draggable="false" alt="{html.escape(caption)}">'
|
| 356 |
f'<div class="garden-caption">{html.escape(caption)}</div>'
|
| 357 |
f'</div>'
|
| 358 |
)
|
| 359 |
+
|
| 360 |
+
# Draw a line between each pair of hand-linked ("neighbor") plants.
|
| 361 |
+
links = []
|
| 362 |
+
drawn = set()
|
| 363 |
+
for p in garden:
|
| 364 |
+
for neighbor_id in p.get("neighbors", []):
|
| 365 |
+
pair = frozenset((p["id"], neighbor_id))
|
| 366 |
+
if pair in drawn or neighbor_id not in positions:
|
| 367 |
+
continue
|
| 368 |
+
drawn.add(pair)
|
| 369 |
+
a, b = positions[p["id"]], positions[neighbor_id]
|
| 370 |
+
links.append(f'<line x1="{a["x"]}" y1="{a["y"]}" x2="{b["x"]}" y2="{b["y"]}" />')
|
| 371 |
+
|
| 372 |
+
links_svg = (
|
| 373 |
+
f'<svg class="garden-links" viewBox="0 0 100 100" preserveAspectRatio="none">{"".join(links)}</svg>'
|
| 374 |
+
if links else ""
|
| 375 |
+
)
|
| 376 |
+
return f'<div class="garden-board-inner">{links_svg}{"".join(tiles)}</div>'
|
| 377 |
|
| 378 |
def _visible_plants(user_id: str) -> list[dict]:
|
| 379 |
"""All garden plants, in the same order as the gallery."""
|
|
|
|
| 557 |
with gr.Row():
|
| 558 |
|
| 559 |
with gr.Column(scale=3):
|
| 560 |
+
with gr.Row():
|
| 561 |
+
gr.Markdown("_Drag a plant to place it anywhere in your garden, or click it to see details._")
|
| 562 |
+
link_mode_btn = gr.Button("🔗 Relier des plantes", elem_id="link-mode-btn", size="sm", scale=0)
|
| 563 |
|
| 564 |
# Garden board — freely draggable pixel-art sprites
|
| 565 |
garden_board = gr.HTML(value="", elem_id="garden-board")
|
|
|
|
| 572 |
board_move_x = gr.Number(value=0, visible=False)
|
| 573 |
board_move_y = gr.Number(value=0, visible=False)
|
| 574 |
board_move_btn = gr.Button("sync", elem_id="garden-move-btn", elem_classes=["board-sync"])
|
| 575 |
+
board_link_idx1 = gr.Number(value=-1, visible=False)
|
| 576 |
+
board_link_idx2 = gr.Number(value=-1, visible=False)
|
| 577 |
+
board_link_btn = gr.Button("sync", elem_id="garden-link-btn", elem_classes=["board-sync"])
|
| 578 |
|
| 579 |
# Detail panel — appears on click
|
| 580 |
plant_detail = gr.Markdown("_Click a plant photo to see its details._", elem_id="plant-detail")
|
|
|
|
| 668 |
js="(idx, x, y, user_id) => { const d = window._gardenMove || {idx: -1, x: 0, y: 0}; return [d.idx, d.x, d.y, user_id]; }",
|
| 669 |
)
|
| 670 |
|
| 671 |
+
def toggle_plant_link(idx1, idx2, user_id):
|
| 672 |
+
"""Toggle a 'neighbor' link between two plants on the board."""
|
| 673 |
+
idx1, idx2 = int(idx1), int(idx2)
|
| 674 |
+
garden = load_garden(user_id)
|
| 675 |
+
if 0 <= idx1 < len(garden) and 0 <= idx2 < len(garden) and idx1 != idx2:
|
| 676 |
+
id1, id2 = garden[idx1]["id"], garden[idx2]["id"]
|
| 677 |
+
neighbors1 = garden[idx1].setdefault("neighbors", [])
|
| 678 |
+
neighbors2 = garden[idx2].setdefault("neighbors", [])
|
| 679 |
+
if id2 in neighbors1:
|
| 680 |
+
neighbors1.remove(id2)
|
| 681 |
+
neighbors2.remove(id1)
|
| 682 |
+
else:
|
| 683 |
+
neighbors1.append(id2)
|
| 684 |
+
neighbors2.append(id1)
|
| 685 |
+
save_garden(garden, user_id)
|
| 686 |
+
return get_garden_board_html(user_id)
|
| 687 |
+
|
| 688 |
+
board_link_btn.click(
|
| 689 |
+
fn=toggle_plant_link,
|
| 690 |
+
inputs=[board_link_idx1, board_link_idx2, user_id_state],
|
| 691 |
+
outputs=[garden_board],
|
| 692 |
+
js="(idx1, idx2, user_id) => { const d = window._gardenLink || {idx1: -1, idx2: -1}; return [d.idx1, d.idx2, user_id]; }",
|
| 693 |
+
)
|
| 694 |
+
|
| 695 |
def _mark_watered_by_idx(idx, user_id):
|
| 696 |
if idx is None:
|
| 697 |
return "⚠️ Select a plant first.", get_garden_board_html(user_id)
|
|
|
|
| 717 |
return "⚠️ Plant not found.", get_garden_board_html(user_id)
|
| 718 |
target_id = visible[idx]["id"]
|
| 719 |
garden = [p for p in load_garden(user_id) if p.get("id") != target_id]
|
| 720 |
+
for p in garden:
|
| 721 |
+
if target_id in p.get("neighbors", []):
|
| 722 |
+
p["neighbors"].remove(target_id)
|
| 723 |
save_garden(garden, user_id)
|
| 724 |
return "🗑️ Plant removed.", get_garden_board_html(user_id)
|
| 725 |
|
|
|
|
| 746 |
return "Select a plant first."
|
| 747 |
plant = plants[idx]
|
| 748 |
info = get_plant_info(plant["genus"])
|
| 749 |
+
by_id = {p["id"]: p for p in plants}
|
| 750 |
+
neighbors = [
|
| 751 |
+
{"name": n.get("nickname") or n["genus"], "genus": n["genus"]}
|
| 752 |
+
for nid in plant.get("neighbors", [])
|
| 753 |
+
if (n := by_id.get(nid)) is not None
|
| 754 |
+
]
|
| 755 |
return advisor.ask_about_plant(
|
| 756 |
question, info,
|
| 757 |
plant_name=plant.get("nickname"),
|
| 758 |
genus=plant["genus"],
|
| 759 |
last_watered=plant.get("last_watered"),
|
| 760 |
+
neighbors=neighbors,
|
| 761 |
)
|
| 762 |
|
| 763 |
advisor_ask_btn.click(
|
modules/advisor.py
CHANGED
|
@@ -32,8 +32,13 @@ def _build_system_prompt(
|
|
| 32 |
plant_name: str | None = None,
|
| 33 |
genus: str | None = None,
|
| 34 |
last_watered: str | None = None,
|
|
|
|
| 35 |
) -> str:
|
| 36 |
name = plant_name or genus or "this plant"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
return (
|
| 38 |
"You are an expert gardening assistant with deep knowledge of houseplant "
|
| 39 |
"and garden plant care. Be practical, encouraging, and specific.\n\n"
|
|
@@ -43,7 +48,8 @@ def _build_system_prompt(
|
|
| 43 |
f"- Soil: {plant_info.get('soil')}\n"
|
| 44 |
f"- Watering frequency: every {plant_info.get('watering_frequency_days')} days\n"
|
| 45 |
f"- Fertilization: {plant_info.get('fertilization_type')}\n"
|
| 46 |
-
f"- Watering status: {_watering_status(last_watered)}\n
|
|
|
|
| 47 |
"Always factor the watering status into your answer: if it is overdue "
|
| 48 |
"compared to the recommended frequency, say so and recommend watering; "
|
| 49 |
"if it was watered recently, take that into account (don't tell the "
|
|
@@ -51,7 +57,10 @@ def _build_system_prompt(
|
|
| 51 |
"possible cause if the question describes a problem.\n\n"
|
| 52 |
"Use this profile as context, but also draw on your general gardening "
|
| 53 |
"knowledge for issues it doesn't cover (pests, diseases, yellowing "
|
| 54 |
-
"leaves, repotting, etc.).
|
|
|
|
|
|
|
|
|
|
| 55 |
"focused answer and never recommend dangerous or toxic substances. "
|
| 56 |
"Answer in 2-4 sentences, in the same language as the question."
|
| 57 |
)
|
|
@@ -63,12 +72,13 @@ def ask_about_plant(
|
|
| 63 |
plant_name: str | None = None,
|
| 64 |
genus: str | None = None,
|
| 65 |
last_watered: str | None = None,
|
|
|
|
| 66 |
) -> str:
|
| 67 |
"""Ask the advisor a question about a specific plant, grounded in its care data."""
|
| 68 |
try:
|
| 69 |
completion = _get_client().chat_completion(
|
| 70 |
messages=[
|
| 71 |
-
{"role": "system", "content": _build_system_prompt(plant_info, plant_name, genus, last_watered)},
|
| 72 |
{"role": "user", "content": question},
|
| 73 |
],
|
| 74 |
max_tokens=300,
|
|
|
|
| 32 |
plant_name: str | None = None,
|
| 33 |
genus: str | None = None,
|
| 34 |
last_watered: str | None = None,
|
| 35 |
+
neighbors: list[dict] | None = None,
|
| 36 |
) -> str:
|
| 37 |
name = plant_name or genus or "this plant"
|
| 38 |
+
neighbor_line = ""
|
| 39 |
+
if neighbors:
|
| 40 |
+
names = ", ".join(f"{n['name']} ({n['genus']})" for n in neighbors)
|
| 41 |
+
neighbor_line = f"- Plants growing right next to it in the garden: {names}\n"
|
| 42 |
return (
|
| 43 |
"You are an expert gardening assistant with deep knowledge of houseplant "
|
| 44 |
"and garden plant care. Be practical, encouraging, and specific.\n\n"
|
|
|
|
| 48 |
f"- Soil: {plant_info.get('soil')}\n"
|
| 49 |
f"- Watering frequency: every {plant_info.get('watering_frequency_days')} days\n"
|
| 50 |
f"- Fertilization: {plant_info.get('fertilization_type')}\n"
|
| 51 |
+
f"- Watering status: {_watering_status(last_watered)}\n"
|
| 52 |
+
f"{neighbor_line}\n"
|
| 53 |
"Always factor the watering status into your answer: if it is overdue "
|
| 54 |
"compared to the recommended frequency, say so and recommend watering; "
|
| 55 |
"if it was watered recently, take that into account (don't tell the "
|
|
|
|
| 57 |
"possible cause if the question describes a problem.\n\n"
|
| 58 |
"Use this profile as context, but also draw on your general gardening "
|
| 59 |
"knowledge for issues it doesn't cover (pests, diseases, yellowing "
|
| 60 |
+
"leaves, repotting, etc.). If nearby plants are listed, factor in "
|
| 61 |
+
"companion-planting effects (competition for light, water or nutrients, "
|
| 62 |
+
"shared pests/diseases, or beneficial pairings) when relevant to the "
|
| 63 |
+
"question. Give a precise, actionable, gardening-advice "
|
| 64 |
"focused answer and never recommend dangerous or toxic substances. "
|
| 65 |
"Answer in 2-4 sentences, in the same language as the question."
|
| 66 |
)
|
|
|
|
| 72 |
plant_name: str | None = None,
|
| 73 |
genus: str | None = None,
|
| 74 |
last_watered: str | None = None,
|
| 75 |
+
neighbors: list[dict] | None = None,
|
| 76 |
) -> str:
|
| 77 |
"""Ask the advisor a question about a specific plant, grounded in its care data."""
|
| 78 |
try:
|
| 79 |
completion = _get_client().chat_completion(
|
| 80 |
messages=[
|
| 81 |
+
{"role": "system", "content": _build_system_prompt(plant_info, plant_name, genus, last_watered, neighbors)},
|
| 82 |
{"role": "user", "content": question},
|
| 83 |
],
|
| 84 |
max_tokens=300,
|
static/style.css
CHANGED
|
@@ -185,6 +185,41 @@ footer {
|
|
| 185 |
opacity: 0.85;
|
| 186 |
}
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
.garden-sprite img {
|
| 189 |
width: 88px;
|
| 190 |
height: 88px;
|
|
|
|
| 185 |
opacity: 0.85;
|
| 186 |
}
|
| 187 |
|
| 188 |
+
#garden-board.link-mode .garden-sprite {
|
| 189 |
+
cursor: crosshair;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.garden-sprite.link-source img {
|
| 193 |
+
outline: 3px solid #43a047;
|
| 194 |
+
outline-offset: 2px;
|
| 195 |
+
border-radius: 8px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* SVG overlay drawing lines between hand-linked ("neighbor") plants */
|
| 199 |
+
.garden-links {
|
| 200 |
+
position: absolute;
|
| 201 |
+
inset: 0;
|
| 202 |
+
width: 100%;
|
| 203 |
+
height: 100%;
|
| 204 |
+
pointer-events: none;
|
| 205 |
+
z-index: 0;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.garden-links line {
|
| 209 |
+
stroke: #8d6e63;
|
| 210 |
+
stroke-width: 2;
|
| 211 |
+
stroke-dasharray: 5 4;
|
| 212 |
+
stroke-linecap: round;
|
| 213 |
+
vector-effect: non-scaling-stroke;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* "Relier des plantes" toggle button */
|
| 217 |
+
#link-mode-btn.link-mode-active {
|
| 218 |
+
background: linear-gradient(135deg, #66bb6a 0%, #43a047 100%) !important;
|
| 219 |
+
color: #ffffff !important;
|
| 220 |
+
border-color: transparent !important;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
.garden-sprite img {
|
| 224 |
width: 88px;
|
| 225 |
height: 88px;
|