From a06dcce3f2a1c4529ddc771051cae1ec8f66a6c0 Mon Sep 17 00:00:00 2001 From: Scott Kilgore Date: Sat, 8 Nov 2025 10:49:47 -0600 Subject: [PATCH] feat: add standalone full-calendar runner and all-day selection support --- README.md | 37 ++ assets/fullcalendar_dark.css | 111 ++++ data/__pycache__/api.cpython-313.pyc | Bin 0 -> 3342 bytes data/api.py | 65 ++- .../__pycache__/intro.cpython-313.pyc | Bin 0 -> 7744 bytes docs/dash_dynamic_grid_layout/intro.py | 39 +- .../__pycache__/api_example.cpython-313.pyc | Bin 0 -> 21382 bytes .../__pycache__/extra_fields.cpython-313.pyc | Bin 0 -> 8320 bytes .../header_toolbar.cpython-313.pyc | Bin 0 -> 5967 bytes .../__pycache__/introduction.cpython-313.pyc | Bin 0 -> 17131 bytes .../section_renders.cpython-313.pyc | Bin 0 -> 5734 bytes docs/full_calendar_component/api_example.py | 517 ++++++++++++++++-- docs/full_calendar_component/extra_fields.py | 226 +++++++- .../full_calendar_components.md | 68 ++- .../full_calendar_component/header_toolbar.py | 265 ++++----- docs/full_calendar_component/introduction.py | 299 +++++++--- .../section_renders.py | 334 ++++------- pages/home.md | 3 +- requirements.txt | 6 +- .../__pycache__/run.cpython-313.pyc | Bin 0 -> 4679 bytes runners/full_calendar/requirements.txt | 6 + runners/full_calendar/run.py | 123 +++++ 22 files changed, 1586 insertions(+), 513 deletions(-) create mode 100644 README.md create mode 100644 assets/fullcalendar_dark.css create mode 100644 data/__pycache__/api.cpython-313.pyc create mode 100644 docs/dash_dynamic_grid_layout/__pycache__/intro.cpython-313.pyc create mode 100644 docs/full_calendar_component/__pycache__/api_example.cpython-313.pyc create mode 100644 docs/full_calendar_component/__pycache__/extra_fields.cpython-313.pyc create mode 100644 docs/full_calendar_component/__pycache__/header_toolbar.cpython-313.pyc create mode 100644 docs/full_calendar_component/__pycache__/introduction.cpython-313.pyc create mode 100644 docs/full_calendar_component/__pycache__/section_renders.cpython-313.pyc create mode 100644 runners/full_calendar/__pycache__/run.cpython-313.pyc create mode 100644 runners/full_calendar/requirements.txt create mode 100644 runners/full_calendar/run.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..af19c2b --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# pip-docs Mini Runners + +This repository powers the Pip Install Python Components docs site. To make it easier to test an individual page without installing the full dependency set, each standalone Dash demo lives in its own “runner” folder with a minimal `requirements.txt`. + +## Available runners + +| Runner | Description | Command | +| --- | --- | --- | +| `runners/full_calendar` | Dash FullCalendar showcase (interactive builder, advanced workflows, API modal demo) | `python runners/full_calendar/run.py` | + +## Running a runner + +1. Create/activate a virtual environment (recommended): + ```bash + cd pip-docs + python -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` +2. Install only the deps needed for the runner you want to test: + ```bash + pip install -r runners/full_calendar/requirements.txt + ``` +3. Start the Dash app: + ```bash + python runners/full_calendar/run.py + ``` + The app listens on `http://127.0.0.1:8059` by default. + +Each runner automatically adds the repo root to `PYTHONPATH` and reuses the shared `assets/` directory, so the demos behave the same way they do inside the full docs build. + +## Adding a new runner + +1. Create `runners//run.py` and add a minimal `requirements.txt`. +2. In `run.py`, import `Path`/`sys`, push the repo root onto `sys.path`, and point Dash at `assets_folder=.../assets`. +3. Document the runner in the table above so contributors know how to start it. + +This pattern keeps the install footprint tiny when you only need to verify one page, while preserving the standard docs structure (Markdown pages, components, data helpers, shared assets, etc.). diff --git a/assets/fullcalendar_dark.css b/assets/fullcalendar_dark.css new file mode 100644 index 0000000..1b185fe --- /dev/null +++ b/assets/fullcalendar_dark.css @@ -0,0 +1,111 @@ +.dark-calendar { + --fc-page-bg-color: #101113; + --fc-neutral-bg-color: #1a1b1e; + --fc-neutral-text-color: #f1f3f5; + --fc-border-color: #2c2e33; + --fc-button-text-color: #f1f3f5; + --fc-button-bg-color: #2c2e33; + --fc-button-border-color: #373a40; + --fc-event-text-color: #f8f9fa; + background-color: #101113; + padding: 0.75rem; + border-radius: 12px; + border: 1px solid #2c2e33; +} + +.dark-calendar .fc, +.dark-calendar .fc-multimonth, +.dark-calendar .fc-multimonth-day, +.dark-calendar .fc-daygrid, +.dark-calendar .fc-daygrid-day { + background-color: rgba(255, 255, 255, 0); + color: #f1f3f5; +} + +.dark-calendar .fc .fc-multimonth, +.dark-calendar .fc .fc-multimonth-grid, +.dark-calendar .fc .fc-multimonth-month { + background-color: #101113; + color: #f1f3f5; +} + +.dark-calendar .fc .fc-multimonth-view, +.dark-calendar .fc .fc-multimonth-view .fc-scrollgrid, +.dark-calendar .fc .fc-multimonth-view table, +.dark-calendar .fc .fc-multimonth-view td, +.dark-calendar .fc .fc-multimonth-view th { + background-color: #101113; + color: #f1f3f5; + border-color: #2c2e33; +} + +.dark-calendar .fc .fc-multimonth .fc-scrollgrid, +.dark-calendar .fc .fc-multimonth .fc-scrollgrid-section, +.dark-calendar .fc .fc-multimonth .fc-scrollgrid-section > td, +.dark-calendar .fc .fc-multimonth .fc-scrollgrid-sync-table, +.dark-calendar .fc .fc-multimonth .fc-scrollgrid-sync-inner { + background-color: #101113; + color: #f1f3f5; + border-color: #2c2e33; +} + +.dark-calendar .fc .fc-multimonth .fc-scrollgrid-section-header, +.dark-calendar .fc .fc-multimonth .fc-scrollgrid-section-header td, +.dark-calendar .fc .fc-multimonth .fc-col-header-cell { + background-color: #101113; + color: #f8f9fa; + border-color: #2c2e33; +} + +.dark-calendar .fc .fc-multimonth .fc-col-header-cell-cushion { + color: #f8f9fa; +} + +.dark-calendar .fc-multimonth-month, +.dark-calendar .fc-multimonth-daygrid, +.dark-calendar .fc-multimonth-daygrid td, +.dark-calendar .fc-multimonth-dayframe { + background-color: #16181c; + border-color: #2c2e33; +} + +.dark-calendar .fc-theme-standard td, +.dark-calendar .fc-theme-standard th { + border-color: #2c2e33; +} + +.dark-calendar .fc-toolbar-title, +.dark-calendar .fc-col-header-cell-cushion, +.dark-calendar .fc-daygrid-day-number, +.dark-calendar .fc-multimonth-title, +.dark-calendar .fc-multimonth-daygrid .fc-daygrid-day-number, +.dark-calendar .fc-list-event-title, +.dark-calendar .fc-list-day-text { + color: #f8f9fa; +} + +.dark-calendar .fc-button { + background-color: #2c2e33; + border-color: #373a40; + color: #f1f3f5; +} + +.dark-calendar .fc-button:hover { + background-color: #3d3f44; +} + +.dark-calendar .fc-multimonth-title { + background-color: transparent; + border-color: #2c2e33; + color: #f8f9fa; +} + +.dark-calendar .fc-list-empty, +.dark-calendar .fc-multimonth-singlecol { + background-color: #16181c; +} + +.dark-calendar .fc-event, +.dark-calendar .fc-h-event { + border: none; +} diff --git a/data/__pycache__/api.cpython-313.pyc b/data/__pycache__/api.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..843626b8d2c721080586fb7887a183257776ffac GIT binary patch literal 3342 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 V0-M}Eb1%=-X?u5E0BK3?{{R5CN~-_> literal 0 HcmV?d00001 diff --git a/docs/dash_dynamic_grid_layout/intro.py b/docs/dash_dynamic_grid_layout/intro.py index 6c3e8d1..8a8e0b6 100644 --- a/docs/dash_dynamic_grid_layout/intro.py +++ b/docs/dash_dynamic_grid_layout/intro.py @@ -10,7 +10,7 @@ import json import random import string -import full_calendar_component as fcc +import dash_fullcalendar as dcal # Sample data for the graph df = px.data.iris() @@ -121,20 +121,29 @@ def generate_random_string(length): id="draggable-color-picker", ), dgl.DraggableWrapper( - fcc.FullCalendarComponent( - id="api_calendar", - initialView='dayGridMonth', - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", + html.Div( + dcal.FullCalendar( + id="api_calendar", + initialView='dayGridMonth', + headerToolbar={ + "left": "prev,next today", + "center": "", + "right": "", + }, + initialDate=f"{formatted_date}", + editable=True, + selectable=True, + events=[], + nowIndicator=True, + navLinks=True, + ), + className="dark-calendar", + style={ + "--fc-page-bg-color": "#101113", + "--fc-neutral-bg-color": "#1a1b1e", + "--fc-neutral-text-color": "#f1f3f5", + "--fc-border-color": "#2c2e33", }, - initialDate=f"{formatted_date}", - editable=True, - selectable=True, - events=[], - nowIndicator=True, - navLinks=True, ), id="draggable-calendar" ), @@ -229,4 +238,4 @@ def remove_component(key, layout): del items[i] break return items - return no_update \ No newline at end of file + return no_update diff --git a/docs/full_calendar_component/__pycache__/api_example.cpython-313.pyc b/docs/full_calendar_component/__pycache__/api_example.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c81a57664da60d6396b2ae4f55b41c32b3b930b GIT binary patch literal 21382 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 Wd;j~_O$QTxTc_^x$8c~u%>6%~J{`OO literal 0 HcmV?d00001 diff --git a/docs/full_calendar_component/__pycache__/introduction.cpython-313.pyc b/docs/full_calendar_component/__pycache__/introduction.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cbb27dd9364df7c4d52e5b18d15965a0713c30e GIT binary patch literal 17131 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 literal 0 HcmV?d00001 diff --git a/docs/full_calendar_component/api_example.py b/docs/full_calendar_component/api_example.py index 292279d..1c9a92a 100644 --- a/docs/full_calendar_component/api_example.py +++ b/docs/full_calendar_component/api_example.py @@ -1,21 +1,295 @@ -import full_calendar_component as fcc -from dash import * +import re +from datetime import datetime, timedelta +from html import unescape +from html.parser import HTMLParser +from urllib.parse import urlparse + +import dash_fullcalendar as dcal +from dash import Input, Output, State, callback, callback_context, dcc, html, no_update import dash_mantine_components as dmc from dash.exceptions import PreventUpdate -from datetime import datetime, date, timedelta from data import api +CALENDAR_STYLE = { + "--fc-page-bg-color": "#101113", + "--fc-neutral-bg-color": "#1a1b1e", + "--fc-neutral-text-color": "#f1f3f5", + "--fc-border-color": "#2c2e33", + "--fc-button-text-color": "#f1f3f5", + "--fc-button-bg-color": "#2c2e33", + "--fc-button-border-color": "#373a40", + "--fc-event-text-color": "#f8f9fa", +} + +CARD_STYLE = { + "backgroundColor": "var(--mantine-color-dark-6)", + "border": "1px solid var(--mantine-color-dark-4)", + "color": "var(--mantine-color-gray-0)", +} + +FIELDSET_STYLES = { + "root": { + **CARD_STYLE, + "borderRadius": "var(--mantine-radius-md)", + "padding": "var(--mantine-spacing-lg)", + }, +} + +CATEGORY_OPTIONS = [ + {"value": "plotly", "label": "Plotly"}, + {"value": "dash", "label": "Dash"}, + {"value": "python", "label": "Python"}, +] + +VIEW_OPTIONS = [ + {"label": "Month", "value": "dayGridMonth"}, + {"label": "Week", "value": "timeGridWeek"}, + {"label": "Day", "value": "timeGridDay"}, + {"label": "List", "value": "listWeek"}, +] + +DEFAULT_CONTEXT_PLACEHOLDER = "No description provided." +SAFE_PROTOCOLS = {"http", "https", "mailto", "tel", ""} +TAG_REGEX = re.compile(r"<[^>]+>") + + +class _SafeHTMLToMarkdown(HTMLParser): + """Convert limited HTML snippets into Markdown while stripping unsafe markup.""" + + _BLOCK_TAGS = { + "p", + "div", + "section", + "article", + "header", + "footer", + } + _SKIP_TAGS = {"script", "style"} + _BOLD_TAGS = {"strong", "b"} + _ITALIC_TAGS = {"em", "i"} + _CODE_TAGS = {"code", "kbd", "samp"} + + def __init__(self): + super().__init__(convert_charrefs=True) + self.fragments = [] + self.anchor_stack = [] + self.list_stack = [] + self.skip_depth = 0 + + def handle_starttag(self, tag, attrs): + tag = tag.lower() + if tag in self._SKIP_TAGS: + self.skip_depth += 1 + return + if self.skip_depth: + return + + if tag == "br": + self.fragments.append(" \n") + elif tag in self._BOLD_TAGS: + self.fragments.append("**") + elif tag in self._ITALIC_TAGS: + self.fragments.append("*") + elif tag in self._CODE_TAGS: + self.fragments.append("`") + elif tag == "a": + href = "" + for attr, value in attrs: + if attr.lower() == "href": + href = _sanitize_href(value) + break + self.anchor_stack.append(href) + self.fragments.append("[") + elif tag == "ul": + self.list_stack.append({"ordered": False, "count": 0}) + elif tag == "ol": + self.list_stack.append({"ordered": True, "count": 0}) + elif tag == "li": + if not self.list_stack: + self.list_stack.append({"ordered": False, "count": 0}) + entry = self.list_stack[-1] + entry["count"] += 1 + bullet = f"{entry['count']}." if entry["ordered"] else "-" + indent = " " * (len(self.list_stack) - 1) + if not self.fragments or not self.fragments[-1].endswith("\n"): + self.fragments.append("\n") + self.fragments.append(f"{indent}{bullet} ") + elif tag in self._BLOCK_TAGS: + if self.fragments and not self.fragments[-1].endswith("\n\n"): + if not self.fragments[-1].endswith("\n"): + self.fragments.append("\n") + if not self.fragments[-1].endswith("\n\n"): + self.fragments.append("\n") + + def handle_endtag(self, tag): + tag = tag.lower() + if tag in self._SKIP_TAGS: + if self.skip_depth: + self.skip_depth -= 1 + return + if self.skip_depth: + return + + if tag in self._BOLD_TAGS: + self.fragments.append("**") + elif tag in self._ITALIC_TAGS: + self.fragments.append("*") + elif tag in self._CODE_TAGS: + self.fragments.append("`") + elif tag == "a": + href = self.anchor_stack.pop() if self.anchor_stack else "" + if href: + self.fragments.append(f"]({href})") + else: + self.fragments.append("]") + elif tag == "li": + if not self.fragments or not self.fragments[-1].endswith("\n"): + self.fragments.append("\n") + elif tag in {"ul", "ol"}: + if self.list_stack: + self.list_stack.pop() + if not self.fragments or not self.fragments[-1].endswith("\n"): + self.fragments.append("\n") + elif tag in self._BLOCK_TAGS: + if not self.fragments or not self.fragments[-1].endswith("\n"): + self.fragments.append("\n") + if not self.fragments[-1].endswith("\n\n"): + self.fragments.append("\n") + + def handle_data(self, data): + if self.skip_depth or not data: + return + normalized = data.replace("\xa0", " ") + if not normalized.strip(): + if self.fragments and not self.fragments[-1].endswith((" ", "\n")): + self.fragments.append(" ") + return + normalized = re.sub(r"\s+", " ", normalized) + self.fragments.append(normalized) + + def get_value(self): + text = "".join(self.fragments) + text = re.sub(r"[ \t]+\n", "\n", text) + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip() + + +def _sanitize_href(value): + if not value: + return "" + candidate = value.strip() + if not candidate: + return "" + if candidate.lower().startswith("javascript:"): + return "" + try: + parsed = urlparse(candidate) + except ValueError: + return "" + scheme = (parsed.scheme or "").lower() + if scheme and scheme not in SAFE_PROTOCOLS: + return "" + return candidate + + +def _strip_tags(value): + if not value: + return "" + without_tags = TAG_REGEX.sub(" ", value) + without_tags = unescape(without_tags) + without_tags = re.sub(r"\s+", " ", without_tags) + return without_tags.strip() -# Get today's date -today = datetime.now() -# Format the date -formatted_date = today.strftime("%Y-%m-%d") +def _to_markdown(value): + if not value: + return DEFAULT_CONTEXT_PLACEHOLDER + parser = _SafeHTMLToMarkdown() + try: + parser.feed(str(value)) + parser.close() + except Exception: + cleaned = _strip_tags(str(value)) + return cleaned or DEFAULT_CONTEXT_PLACEHOLDER -events = api.get_events_by_category("plotly") + cleaned = parser.get_value() + if cleaned: + return cleaned + cleaned = _strip_tags(str(value)) + return cleaned or DEFAULT_CONTEXT_PLACEHOLDER -component = html.Div([ +def _render_description(markdown_text, *, style=None): + return dcc.Markdown(markdown_text, style=style or {}, link_target="_blank") + + +def _load_events(categories): + categories = categories or ["plotly"] + combined = [] + for cat in categories: + dataset = api.get_events_by_category(cat) or [] + for idx, event in enumerate(dataset): + normalized = { + "title": event.get("title") or f"{cat.title()} event", + "start": event.get("start"), + "end": event.get("end"), + "allDay": event.get("allDay", False), + "className": event.get("className", "bg-gradient-primary"), + "extendedProps": {**event.get("extendedProps", {}), "category": cat.title()}, + "id": event.get("id") or f"{cat}-{idx}", + } + combined.append(normalized) + combined.sort(key=lambda e: e.get("start") or "") + return _normalize_event_dates(combined) + + +def _normalize_event_dates(events): + if not events: + return events + + today = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) + normalized = [] + for idx, event in enumerate(events): + start_iso = event.get("start") + end_iso = event.get("end") + + target_day = today + timedelta(days=idx) + + def parse_iso(value): + if not value: + return None + try: + cleaned = value.replace("Z", "") + return datetime.fromisoformat(cleaned) + except ValueError: + return None + + start_dt = parse_iso(start_iso) + end_dt = parse_iso(end_iso) + + if start_dt: + new_start = datetime.combine(target_day.date(), start_dt.time()) + else: + new_start = target_day + + if end_dt: + duration = end_dt - start_dt if start_dt else timedelta(hours=1) + new_end = new_start + duration + else: + new_end = new_start + timedelta(hours=1) + + event = {**event, "start": new_start.isoformat(), "end": new_end.isoformat()} + normalized.append(event) + + return normalized + + +formatted_date = datetime.now().strftime("%Y-%m-%d") +initial_events = _load_events(["plotly"]) + + +component = html.Div( + [ dmc.MantineProvider( theme={"colorScheme": "dark"}, children=[ @@ -43,58 +317,215 @@ ) ], ), - fcc.FullCalendarComponent( - id="api_calendar", # Unique ID for the component - initialView='dayGridMonth', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates + dmc.Stack( + [ + dmc.Fieldset( + legend="API Controls", + styles=FIELDSET_STYLES, + children=[ + dmc.MultiSelect( + label="Categories", + id="api_category_select", + data=CATEGORY_OPTIONS, + value=["plotly"], + clearable=False, + ), + dmc.Group( + [ + dmc.SegmentedControl( + id="api_view_control", + data=VIEW_OPTIONS, + value="dayGridMonth", + color="indigo", + ), + dmc.Button( + "Refresh events", + id="api_refresh_button", + variant="outline", + ), + ], + justify="space-between", + ), + dmc.Group( + [ + dmc.Switch(label="Show weekends", id="api_toggle_weekends", checked=True, color="indigo"), + dmc.Switch(label="Enable nav links", id="api_toggle_navlinks", checked=True, color="indigo"), + dmc.Switch(label="Allow selection", id="api_toggle_selectable", checked=True, color="indigo"), + ], + justify="flex-start", + ), + ], + ), + dmc.Alert( + "Select different data categories, change views, or toggle interactions to see how dash-fullcalendar reacts to API-fed data.", + color="indigo", + variant="light", + radius="md", + ), + dmc.Flex( + [ + dmc.Paper( + html.Div( + dcal.FullCalendar( + id="api_calendar", + initialView="dayGridMonth", + headerToolbar={"left": "prev,next today", "center": "", "right": ""}, + initialDate=formatted_date, + editable=True, + selectable=True, + events=initial_events, + nowIndicator=True, + navLinks=True, + ), + className="dark-calendar", + style=CALENDAR_STYLE, + ), + radius="md", + withBorder=True, + p="md", + style={**CARD_STYLE, "flex": 2}, + ), + dmc.Paper( + dmc.Stack( + [ + dmc.Badge(f"{len(initial_events)} events loaded", id="api_event_count_badge"), + dmc.ScrollArea( + html.Div(id="api_event_summary"), + h=320, + ), + dmc.Divider(label="Last clicked event"), + html.Div( + "Click an event on the calendar to preview its details here.", + id="api_event_preview", + style={"minHeight": "80px"}, + ), + ], + gap="md", + ), + radius="md", + withBorder=True, + p="md", + style={**CARD_STYLE, "flex": 1}, + ), + ], + gap="1.5rem", + direction={"base": "column", "lg": "row"}, + style={"width": "100%"}, + ), + ], + gap="md", + ), + ], + style={"padding": "1.5rem 0"}, +) + + +@callback( + Output("api_calendar", "events"), + Output("api_event_summary", "children"), + Output("api_event_count_badge", "children"), + Input("api_category_select", "value"), + Input("api_refresh_button", "n_clicks"), + prevent_initial_call=False, +) +def update_api_events(categories, _): + events = _load_events(categories) + summary_children = [] + for event in events[:15]: + summary_children.append( + dmc.Paper( + dmc.Group( + [ + dmc.Stack( + [ + dmc.Text(event["title"], fw=600), + dmc.Text( + f"{event.get('start', '')} → {event.get('end', 'open')}", + size="sm", + c="dimmed", + ), + ], + gap=2, + ), + dmc.Badge(event["extendedProps"].get("category", ""), color="violet", variant="light"), + ], + justify="space-between", + ), + withBorder=True, + radius="md", + p="sm", + ) ) - ] + summary = ( + dmc.Stack(summary_children, gap="sm") + if summary_children + else dmc.Text("No events returned from the API.", c="dimmed") + ) + badge_text = f"{len(events)} events loaded" + return events, summary, badge_text + + +@callback( + Output("api_calendar", "weekends"), + Output("api_calendar", "navLinks"), + Output("api_calendar", "selectable"), + Input("api_toggle_weekends", "checked"), + Input("api_toggle_navlinks", "checked"), + Input("api_toggle_selectable", "checked"), +) +def toggle_calendar_behavior(weekends, nav_links, selectable): + return bool(weekends), bool(nav_links), bool(selectable) + + +@callback( + Output("api_calendar", "command"), + Input("api_view_control", "value"), ) +def change_view(view_value): + if not view_value: + return no_update + return {"type": "changeView", "view": view_value} + @callback( Output("api_event_modal", "opened"), Output("api_event_modal", "title"), Output("api_event_modal_display_context", "children"), + Output("api_event_preview", "children"), Input("api_event_modal_close_button", "n_clicks"), - Input("api_calendar", "clickedEvent"), + Input("api_calendar", "eventClick"), State("api_event_modal", "opened"), - prevent_initial_call=True # Set this to True + prevent_initial_call=True, ) -def open__api_event_modal(n, clickedEvent, opened): - +def open__api_event_modal(n, event_click, opened): ctx = callback_context if not ctx.triggered: raise PreventUpdate - else: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] + button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == "api_calendar" and clickedEvent is not None: - event_title = clickedEvent["title"] - event_context = clickedEvent["extendedProps"]["context"] + if button_id == "api_calendar" and event_click is not None: + event_title = event_click.get("title", "Selected event") + extended_props = event_click.get("extendedProps") or {} + event_context = extended_props.get("context", DEFAULT_CONTEXT_PLACEHOLDER) + category = extended_props.get("category", "API") + safe_description = _to_markdown(event_context or "") + preview = dmc.Stack( + [ + dmc.Text(event_title, fw=600), + dmc.Text(f"Category: {category}", size="sm", c="dimmed"), + _render_description(safe_description, style={"fontSize": "0.9rem"}), + ], + gap=4, + ) return ( True, event_title, - html.Div( - html.P(f''' - {event_context} - '''), - style={"width": "100%", "overflowY": "auto"}, - ) + _render_description(safe_description), + preview, ) - elif button_id == "modal-close-button" and n is not None: - return False, dash.no_update, dash.no_update + if button_id == "api_event_modal_close_button" and n is not None: + return False, no_update, no_update, no_update - return opened, dash.no_update, dash.no_update \ No newline at end of file + return opened, no_update, no_update, no_update diff --git a/docs/full_calendar_component/extra_fields.py b/docs/full_calendar_component/extra_fields.py index 6bdf190..9ad57b3 100644 --- a/docs/full_calendar_component/extra_fields.py +++ b/docs/full_calendar_component/extra_fields.py @@ -1,21 +1,229 @@ -from typing import Literal +from datetime import datetime, timedelta +import dash_fullcalendar as dcal import dash_mantine_components as dmc -from dash import Input, Output, State, callback, html +from dash import Input, Output, callback, callback_context, html, no_update +from dash.exceptions import PreventUpdate +BUSINESS_HOURS = [ + {"daysOfWeek": [1, 2, 3, 4, 5], "startTime": "09:00", "endTime": "17:00"}, +] + +CARD_STYLE = { + "backgroundColor": "var(--mantine-color-dark-6)", + "border": "1px solid var(--mantine-color-dark-4)", + "color": "var(--mantine-color-gray-0)", +} + +FIELDSET_STYLES = { + "root": { + **CARD_STYLE, + "borderRadius": "var(--mantine-radius-md)", + "padding": "var(--mantine-spacing-lg)", + } +} + +ALERT_STYLES = {"root": {"color": "var(--mantine-color-dark-9)"}} + +CALENDAR_STYLE = { + "--fc-page-bg-color": "#101113", + "--fc-neutral-bg-color": "#1a1b1e", + "--fc-neutral-text-color": "#f1f3f5", + "--fc-border-color": "#2c2e33", + "--fc-button-text-color": "#f1f3f5", + "--fc-button-bg-color": "#2c2e33", + "--fc-button-border-color": "#373a40", + "--fc-event-text-color": "#f8f9fa", +} + + +def _slot(day_offset, hour, duration, title, color, context): + day = datetime.now().date() + timedelta(days=day_offset) + start = datetime.combine(day, datetime.min.time()) + timedelta(hours=hour) + end = start + timedelta(hours=duration) + return { + "id": title.lower().replace(" ", "-"), + "title": title, + "start": start.strftime("%Y-%m-%dT%H:%M:%S"), + "end": end.strftime("%Y-%m-%dT%H:%M:%S"), + "className": color, + "extendedProps": {"context": context}, + } + + +ADVANCED_EVENTS = [ + _slot(0, 9, 1, "Stand-up", "bg-gradient-primary", "Daily scrum and blockers."), + _slot(0, 13, 1, "Pairing Session", "bg-gradient-info", "Mob pairing on the dashboard."), + _slot(1, 11, 1.5, "Customer Demo", "bg-gradient-success", "Demo latest release to customers."), + _slot(2, 15, 2, "Deep Work", "bg-gradient-warning", "Heads-down time for migration tasks."), + _slot(3, 20, 1, "Deploy", "bg-gradient-danger", "Blue/green switch-over."), +] component = dmc.SimpleGrid( - [ + cols={"base": 1, "lg": 2}, + spacing="2rem", + children=[ dmc.Paper( - html.H1("Hello world, extra_fields.py!"), - id="extra-wrapper", - style={"gridColumn": "1 / 4"}, + [ + dmc.Group( + [ + dmc.Button("Prev", id="advanced-command-prev", variant="light"), + dmc.Button("Today", id="advanced-command-today"), + dmc.Button("Next", id="advanced-command-next", variant="light"), + ], + justify="center", + mb="md", + ), + html.Div( + dcal.FullCalendar( + id="advanced-calendar", + initialView="timeGridWeek", + events=ADVANCED_EVENTS, + selectable=True, + selectMirror=True, + editable=True, + eventDurationEditable=True, + weekends=True, + businessHours=BUSINESS_HOURS, + nowIndicator=True, + height="780px", + ), + className="dark-calendar", + style=CALENDAR_STYLE, + ), + ], + radius="md", + withBorder=True, + p="xl", + style=CARD_STYLE, + ), + dmc.Paper( + dmc.Stack( + [ + dmc.Fieldset( + legend="Toggle calendar behavior", + children=[ + dmc.Switch(label="Show weekends", id="toggle-weekends", checked=True, color="indigo"), + dmc.Switch(label="Enforce business hours", id="toggle-business-hours", checked=True, color="indigo"), + dmc.Switch(label="Allow dragging/resizing", id="toggle-editable", checked=True, color="indigo"), + dmc.Switch(label="Allow range selection", id="toggle-selectable", checked=True, color="indigo"), + ], + styles=FIELDSET_STYLES, + ), + dmc.Divider(label="Live activity"), + dmc.Alert( + "Click a date to capture quick notes.", + id="calendar-click-output", + color="indigo", + variant="light", + radius="md", + styles=ALERT_STYLES, + ), + dmc.Alert( + "Select a range to schedule a block.", + id="calendar-select-output", + color="teal", + variant="light", + radius="md", + styles=ALERT_STYLES, + ), + dmc.Alert( + "Move or resize an event to see the payload.", + id="calendar-mutation-output", + color="yellow", + variant="light", + radius="md", + styles=ALERT_STYLES, + ), + ], + gap="md", + ), + radius="md", + withBorder=True, + p="xl", + style=CARD_STYLE, ), - ], - cols=4, - spacing="2rem", + style={"padding": "1.5rem 0"}, +) + + +@callback( + Output("advanced-calendar", "command"), + Input("advanced-command-prev", "n_clicks"), + Input("advanced-command-today", "n_clicks"), + Input("advanced-command-next", "n_clicks"), + prevent_initial_call=True, +) +def navigate_calendar(prev_clicks, today_clicks, next_clicks): + ctx = callback_context + if not ctx.triggered: + raise PreventUpdate + + trigger = ctx.triggered[0]["prop_id"].split(".")[0] + mapping = { + "advanced-command-prev": "prev", + "advanced-command-today": "today", + "advanced-command-next": "next", + } + return {"type": mapping.get(trigger, "today")} + + +@callback( + Output("advanced-calendar", "weekends"), + Output("advanced-calendar", "businessHours"), + Output("advanced-calendar", "editable"), + Output("advanced-calendar", "selectable"), + Input("toggle-weekends", "checked"), + Input("toggle-business-hours", "checked"), + Input("toggle-editable", "checked"), + Input("toggle-selectable", "checked"), +) +def tune_calendar(weekends, business_hours, editable, selectable): + return ( + bool(weekends), + BUSINESS_HOURS if business_hours else False, + bool(editable), + bool(selectable), + ) + + +@callback( + Output("calendar-click-output", "children"), + Output("calendar-select-output", "children"), + Output("calendar-mutation-output", "children"), + Input("advanced-calendar", "dateClick"), + Input("advanced-calendar", "select"), + Input("advanced-calendar", "eventDrop"), + Input("advanced-calendar", "eventResize"), + Input("advanced-calendar", "eventClick"), + prevent_initial_call=True, ) +def surface_activity(date_click, selection, event_drop, event_resize, event_click): + ctx = callback_context + if not ctx.triggered: + raise PreventUpdate + + prop_id = ctx.triggered[0]["prop_id"] + click_msg = no_update + select_msg = no_update + mutation_msg = no_update + if prop_id == "advanced-calendar.dateClick" and date_click: + click_msg = f"You clicked {date_click}." + elif prop_id == "advanced-calendar.select" and selection: + select_msg = f"Selected {selection['start']} → {selection['end']}." + elif prop_id == "advanced-calendar.eventDrop" and event_drop: + delta = event_drop["delta"] + click_delta = delta.get("days", 0) or delta.get("milliseconds", 0) / (1000 * 60 * 60 * 24) + mutation_msg = f"Moved {event_drop['event']['title']} by {click_delta:.1f} day(s)." + elif prop_id == "advanced-calendar.eventResize" and event_resize: + delta = event_resize["delta"] + hours = delta.get("milliseconds", 0) / (1000 * 60 * 60) + mutation_msg = f"Extended {event_resize['event']['title']} by {hours:.1f} hour(s)." + elif prop_id == "advanced-calendar.eventClick" and event_click: + context = (event_click.get("extendedProps") or {}).get("context", "No notes attached.") + mutation_msg = f"{event_click.get('title', 'Event')}: {context}" + return click_msg, select_msg, mutation_msg diff --git a/docs/full_calendar_component/full_calendar_components.md b/docs/full_calendar_component/full_calendar_components.md index 5d925e5..5bf4f3f 100644 --- a/docs/full_calendar_component/full_calendar_components.md +++ b/docs/full_calendar_component/full_calendar_components.md @@ -1,24 +1,24 @@ --- name: Full Calendar -description: Use Full Calendar Component to create a dash calendar. +description: Use Dash FullCalendar to embed the entire FullCalendar surface inside Dash. endpoint: /pip/full_calendar_component -package: full_calendar_component +package: dash-fullcalendar icon: line-md:calendar --- .. toc:: -[Visit GitHub Repo](https://github.com/pip-install-python/full_calendar_component) +[Visit GitHub Repo](https://github.com/pip-install-python/dash-fullcalendar) ### Installation ```bash -pip install full-calendar-component +pip install dash-fullcalendar ``` ### Introduction -This is an example of a full event calendar with all views: `listWeek`,`timeGridDay`,`timeGridWeek`,`dayGridMonth`. You can add events to your calendar with `Clicking` on a day wich will pop open a modal form for you to enter content for a new event. +`dash-fullcalendar` is a thin wrapper around `@fullcalendar/react`, so every prop and callback you see in the FullCalendar docs is available in Dash. The example below recreates the classic "click to add" workflow with modals, rich text notes, and event details powered by `eventClick`/`dateClick`. .. exec::docs.full_calendar_component.introduction :code: false @@ -27,23 +27,67 @@ This is an example of a full event calendar with all views: `listWeek`,`timeGrid :defaultExpanded: false :withExpandedButton: true -### Render intialView +### Views & Layouts -You have access to these views: `dayGridMonth`, `timeGridWeek`, `timeGridDay`, `listWeek`, `dayGridWeek`, `dayGridYear`, `multiMonthYear`, `resourceTimeline`, `resourceTimeGridDay`, `resourceTimeLineWeek`. You can choose to render all or some of them via passing them as options in the headerToolbar. +Switch between grid, list, multi-month, and Scheduler views by changing `initialView` (or calling the client-side API). Resource timelines automatically load when a Scheduler key plus premium plugins are supplied. .. exec::docs.full_calendar_component.section_renders :code: false -### Header Toolbar +.. sourcetabs::docs/full_calendar_component/section_renders.py + :defaultExpanded: false + :withExpandedButton: true + +.. admonition:: Premium Scheduler plugins + :icon: mdi:star-four-points + :color: yellow + + Timeline and resource views require FullCalendar Scheduler. Use your commercial key or the open-source key (`GPL-My-Project-Is-Open-Source`) together with `plugins=["resourceTimeline","resourceTimeGrid","resource"]`. + +### Header Toolbar & Buttons + +FullCalendar's header is just a config object. Populate the `left`, `center`, and `right` slots with navigation controls, view buttons, or even custom buttons returned from Dash callbacks via the `command` prop. -The Calendar header is broken into 3 parts `left`, `center`, `right`. You can customize the header by passing in the `headerToolbar` prop with the desired configuration `title`, `prev`, `next`, `prevYear`, `today` or in intialView prop `dayGridMonth`, `timeGridWeek`, `timeGridDay`, `listWeek`, `dayGridWeek`, `dayGridYear`, `multiMonthYear`, `resourceTimeline`, `resourceTimeGridDay`, `resourceTimeLineWeek`. .. exec::docs.full_calendar_component.header_toolbar :code: false -### API Example +.. sourcetabs::docs/full_calendar_component/header_toolbar.py + :defaultExpanded: false + :withExpandedButton: true + +### Interactive Workflows + +The advanced example wires every bi-directional callback: `dateClick`, `select`, `eventDrop`, `eventResize`, `eventClick`, and programmatic navigation through the `command` prop. Switches update `weekends`, `businessHours`, `editable`, and `selectable` live. + +.. exec::docs.full_calendar_component.extra_fields + :code: false + +.. sourcetabs::docs/full_calendar_component/extra_fields.py + :defaultExpanded: false + :withExpandedButton: true + +### Remote Data & API Feeds + +Events can arrive from REST, databases, or sockets. Feed a list of dictionaries to the `events` prop, or point `eventSources` to URLs. This example pulls events from a JSON API (and falls back to sample data when offline); click any event to open a Mantine modal showing the extended metadata returned via `eventClick`. .. exec::docs.full_calendar_component.api_example :code: false -### Event -Calendars can have events displayed via a list provided the events prop, which is a list of dictionaries with the following keys: `title`, `start`, `end`, `allDay`, `resourceId`, `classNames`, `display`, `extendedProps`, `id`. Start and end are datetime strings in the format `YYYY-MM-DDTHH:MM:SS` or `YYYY-MM-DD`. AllDay is a boolean. ResourceId is a string. ClassNames is a list of strings. Display is a string. ExtendedProps is a dictionary. Id is a string. +.. sourcetabs::docs/full_calendar_component/api_example.py + :defaultExpanded: false + :withExpandedButton: true + +### Event payloads & callback hooks + +Events supplied to `dash_fullcalendar.FullCalendar` mirror the upstream schema: + +- Required keys: `title`, `start`, optionally `end` (ISO strings), `allDay` (bool) when appropriate. +- Presentation: `className`/`classNames`, `display`, `resourceId`, `color`, `extendedProps` for any custom metadata. +- Interaction payloads: + - `dateClick` → an ISO datetime string of the clicked cell. + - `select` → `{"start": str, "end": str, "allDay": bool}`. + - `eventClick` → `{"title", "start", "end", "allDay", "extendedProps": {...}}`. + - `eventDrop`/`eventResize` → objects containing the new `event`, the `oldEvent`, and `delta`/`relatedEvents`. + - `command` → pass dictionaries such as `{"type": "next"}` or `{"type": "changeView", "view": "timeGridWeek"}` to drive the client API from Dash. + +Build custom forms, dashboards, or schedulers by combining these props with Dash callbacks—everything displayed above is powered by standard Dash patterns plus the new `dash-fullcalendar` component. diff --git a/docs/full_calendar_component/header_toolbar.py b/docs/full_calendar_component/header_toolbar.py index b8b2336..4c598d9 100644 --- a/docs/full_calendar_component/header_toolbar.py +++ b/docs/full_calendar_component/header_toolbar.py @@ -1,162 +1,165 @@ -from typing import Literal +from datetime import datetime, timedelta -import dash +import dash_fullcalendar as dcal import dash_mantine_components as dmc -from dash import * -import full_calendar_component as fcc -from datetime import datetime, date, timedelta -from dash.exceptions import PreventUpdate +from dash import Input, Output, callback, html + +CALENDAR_STYLE = { + "--fc-page-bg-color": "#101113", + "--fc-neutral-bg-color": "#1a1b1e", + "--fc-neutral-text-color": "#f1f3f5", + "--fc-border-color": "#2c2e33", + "--fc-button-text-color": "#f1f3f5", + "--fc-button-bg-color": "#2c2e33", + "--fc-button-border-color": "#373a40", + "--fc-event-text-color": "#f8f9fa", +} + +BASE_HEADER_CHOICES = [ + {"value": "title", "label": "Title"}, + {"value": "prev", "label": "Prev"}, + {"value": "next", "label": "Next"}, + {"value": "prevYear", "label": "Prev Year"}, + {"value": "today", "label": "Today"}, + {"value": "dayGridMonth", "label": "dayGridMonth"}, + {"value": "timeGridWeek", "label": "timeGridWeek"}, + {"value": "timeGridDay", "label": "timeGridDay"}, + {"value": "listWeek", "label": "listWeek"}, + {"value": "listDay", "label": "listDay"}, + {"value": "multiMonthYear", "label": "multiMonthYear"}, +] + +HAS_RESOURCE_API = "resources" in getattr(dcal.FullCalendar, "available_properties", []) + +PREMIUM_CHOICES = [ + {"value": "resourceTimelineWeek", "label": "resourceTimelineWeek*"}, + {"value": "resourceTimeGridDay", "label": "resourceTimeGridDay*"}, +] + +HEADER_CHOICES = BASE_HEADER_CHOICES + (PREMIUM_CHOICES if HAS_RESOURCE_API else []) +PREMIUM_KEYS = {choice["value"] for choice in PREMIUM_CHOICES} if HAS_RESOURCE_API else set() + +RESOURCES = [ + {"id": "room-1", "title": "Room 101"}, + {"id": "room-2", "title": "Room 202"}, + {"id": "hybrid", "title": "Remote Crew"}, +] + + +def _demo_events(): + base_day = datetime.now().date() + + def block(title, day_offset, hour, duration, resource, color, context): + start = datetime.combine(base_day + timedelta(days=day_offset), datetime.min.time()) + timedelta(hours=hour) + end = start + timedelta(hours=duration) + event = { + "title": title, + "start": start.strftime("%Y-%m-%dT%H:%M:%S"), + "end": end.strftime("%Y-%m-%dT%H:%M:%S"), + "className": color, + "extendedProps": {"context": context}, + } + if resource: + event["resourceId"] = resource + return event + + return [ + block("Roadmap sync", 0, 10, 1.5, "room-1", "bg-gradient-success", "Company-wide goals alignment."), + block("Design pairing", 1, 13, 1, "room-2", "bg-gradient-info", "Working session with design."), + block("Support rotation", 1, 16, 2, "hybrid", "bg-gradient-warning", "Handling premium support tickets."), + block("Deploy", 3, 21, 1.5, "room-1", "bg-gradient-danger", "Nightly deployment window."), + ] component = dmc.SimpleGrid( cols={"base": 1, "sm": 1, "lg": 4}, + spacing="2rem", children=[ dmc.Paper( html.Div(id="view-fcc-2"), id="intro-wrapper-fcc-2", style={"gridColumn": "1 / 4"}, + withBorder=True, + radius="md", + p="md", ), dmc.Stack( [ - dmc.MultiSelect( - label="Left of the calendar headerToolbar", - placeholder="Select all you like!", - id="fcc-headerToolbar-left-muti-select", - value=['prev','today','next'], - data=[ - {"value": "title", "label": "title"}, - {"value": "prev", "label": "prev"}, - {"value": "next", "label": "next"}, - {"value": "prevYear", "label": "prevYear"}, - {"value": "today", "label": "today"}, - {"value": "dayGridMonth", "label": "dayGridMonth"}, - {"value": "timeGridWeek", "label": "timeGridWeek"}, - {"value": "timeGridDay", "label": "timeGridDay"}, - {"value": "listWeek", "label": "listWeek"}, - {"value": "dayGridWeek", "label": "dayGridWeek"}, - {"value": "dayGridYear", "label": "dayGridYear"}, - {"value": "multiMonthYear", "label": "multiMonthYear"}, - {"value": "resourceTimeline", "label": "resourceTimeline"}, - {"value": "resourceTimeGridDay", "label": "resourceTimeGridDay"} - ], - mb=10, - ), dmc.MultiSelect( - label="Center of the calendar headerToolbar", - placeholder="Select all you like!", + label="Left of the headerToolbar", + placeholder="Pick buttons", + id="fcc-headerToolbar-left-muti-select", + value=["prev", "today", "next"], + data=HEADER_CHOICES, + mb=10, + ), + dmc.MultiSelect( + label="Center of the headerToolbar", + placeholder="Pick buttons", id="fcc-headerToolbar-center-muti-select", - value=['title'], - data=[ - {"value": "title", "label": "title"}, - {"value": "prev", "label": "prev"}, - {"value": "next", "label": "next"}, - {"value": "prevYear", "label": "prevYear"}, - {"value": "today", "label": "today"}, - {"value": "dayGridMonth", "label": "dayGridMonth"}, - {"value": "timeGridWeek", "label": "timeGridWeek"}, - {"value": "timeGridDay", "label": "timeGridDay"}, - {"value": "listWeek", "label": "listWeek"}, - {"value": "dayGridWeek", "label": "dayGridWeek"}, - {"value": "dayGridYear", "label": "dayGridYear"}, - {"value": "multiMonthYear", "label": "multiMonthYear"}, - {"value": "resourceTimeline", "label": "resourceTimeline"}, - {"value": "resourceTimeGridDay", "label": "resourceTimeGridDay"} - ], + value=["title"], + data=HEADER_CHOICES, mb=10, ), dmc.MultiSelect( - label="Right of the calendar headerToolbar", - placeholder="Select all you like!", + label="Right of the headerToolbar", + placeholder="Pick buttons", id="fcc-headerToolbar-right-muti-select", - value=['timeGridDay', 'dayGridMonth', 'timeGridWeek'], - data=[ - {"value": "title", "label": "title"}, - {"value": "prev", "label": "prev"}, - {"value": "next", "label": "next"}, - {"value": "prevYear", "label": "prevYear"}, - {"value": "today", "label": "today"}, - {"value": "dayGridMonth", "label": "dayGridMonth"}, - {"value": "timeGridWeek", "label": "timeGridWeek"}, - {"value": "timeGridDay", "label": "timeGridDay"}, - {"value": "listWeek", "label": "listWeek"}, - {"value": "dayGridWeek", "label": "dayGridWeek"}, - {"value": "dayGridYear", "label": "dayGridYear"}, - {"value": "multiMonthYear", "label": "multiMonthYear"}, - {"value": "resourceTimeline", "label": "resourceTimeline"}, - {"value": "resourceTimeGridDay", "label": "resourceTimeGridDay"} - ], + value=["dayGridMonth", "timeGridWeek", "timeGridDay", "listWeek"], + data=HEADER_CHOICES, mb=10, - ) - ] + ), + dmc.Text( + "Buttons marked with * require Scheduler plugins and a license key." + if HAS_RESOURCE_API + else "Install the Scheduler build of dash-fullcalendar to unlock resource buttons (*).", + size="sm", + c="dimmed", + ), + ] ), - ], - spacing="2rem", + style={"padding": "1.5rem 0"}, ) + @callback( Output("view-fcc-2", "children"), - # Output('test-out', 'children'), Input("fcc-headerToolbar-left-muti-select", "value"), Input("fcc-headerToolbar-center-muti-select", "value"), Input("fcc-headerToolbar-right-muti-select", "value"), - ) -def update_form(render, render2, render3): - # Get today's date - today = datetime.now() - - # Format the date - formatted_date = today.strftime("%Y-%m-%d") - - events = [ - { - "title": "Pip Install Python", - "start": f"{formatted_date}", - "end": f"{formatted_date}", - "className": "bg-gradient-success", - "context": "Pip Install FullCalendar", - }, - { - 'title': 'Meeting with the boss', - 'start': f"{formatted_date}T14:30:00", - 'end': f"{formatted_date}T15:30:00", - 'className': 'bg-gradient-info', - 'context': 'Meeting with the boss', - }, - { - 'title': 'Happy Hour', - 'start': f"{formatted_date}T17:30:00", - 'end': f"{formatted_date}T18:30:00", - 'className': 'bg-gradient-warning', - 'context': 'Happy Hour', - }, - { - 'title': 'Dinner', - 'start': f"{formatted_date}T20:00:00", - 'end': f"{formatted_date}T21:00:00", - 'className': 'bg-gradient-danger', - 'context': 'Dinner', - } - ] - render_value = ",".join(render) - render2_value = ",".join(render2) - render3_value = ",".join(render3) - - - return fcc.FullCalendarComponent( - id="view-calendar9", # Unique ID for the component - initialView='dayGridMonth', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": render_value, - "center": render2_value, - "right": render3_value, - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) \ No newline at end of file +def update_form(left_buttons, center_buttons, right_buttons): + left = ",".join(left_buttons) if left_buttons else "" + center = ",".join(center_buttons) if center_buttons else "" + right = ",".join(right_buttons) if right_buttons else "" + + events = _demo_events() + calendar_kwargs = { + "id": "view-calendar-toolbar", + "initialView": "dayGridMonth", + "initialDate": events[0]["start"].split("T")[0], + "headerToolbar": {"left": left, "center": center, "right": right}, + "events": events, + "editable": True, + "selectable": True, + "navLinks": True, + "nowIndicator": True, + "height": "650px", + } + + selected = set(left_buttons + center_buttons + right_buttons) + if HAS_RESOURCE_API and (selected & PREMIUM_KEYS): + calendar_kwargs.update( + { + "schedulerLicenseKey": "GPL-My-Project-Is-Open-Source", + "plugins": ["resourceTimeline", "resourceTimeGrid", "resource"], + "resources": RESOURCES, + } + ) + + return html.Div( + dcal.FullCalendar(**calendar_kwargs), + className="dark-calendar", + style=CALENDAR_STYLE, + ) diff --git a/docs/full_calendar_component/introduction.py b/docs/full_calendar_component/introduction.py index 815de04..7478be8 100644 --- a/docs/full_calendar_component/introduction.py +++ b/docs/full_calendar_component/introduction.py @@ -1,9 +1,21 @@ -import full_calendar_component as fcc -from dash import * +from datetime import date, datetime, time, timedelta + +import dash_fullcalendar as dcal import dash_mantine_components as dmc -from dash.exceptions import PreventUpdate -from datetime import datetime, date, timedelta import dash_quill +from dash import Input, Output, State, callback, callback_context, dcc, html, no_update +from dash.exceptions import PreventUpdate + +CALENDAR_STYLE = { + "--fc-page-bg-color": "#101113", + "--fc-neutral-bg-color": "#1a1b1e", + "--fc-neutral-text-color": "#f1f3f5", + "--fc-border-color": "#2c2e33", + "--fc-button-text-color": "#f1f3f5", + "--fc-button-bg-color": "#2c2e33", + "--fc-button-border-color": "#373a40", + "--fc-event-text-color": "#f8f9fa", +} quill_mods = [ [{"header": "1"}, {"header": "2"}, {"font": []}], @@ -19,23 +31,150 @@ # Format the date formatted_date = today.strftime("%Y-%m-%d") + +def _parse_calendar_datetime(value): + """Best-effort parser for FullCalendar ISO strings or datetime objects.""" + if not value: + return None + if isinstance(value, datetime): + return value + if isinstance(value, time): + return datetime.combine(datetime.now().date(), value) + if isinstance(value, date): + return datetime.combine(value, datetime.min.time()) + if isinstance(value, (int, float)): + return datetime.fromtimestamp(value) + if isinstance(value, str): + cleaned = value.strip() + if cleaned.endswith("Z"): + cleaned = cleaned[:-1] + "+00:00" + try: + parsed = datetime.fromisoformat(cleaned) + except ValueError: + try: + parsed = datetime.strptime(cleaned, "%Y-%m-%d") + except ValueError: + return None + if parsed.tzinfo: + return parsed.astimezone().replace(tzinfo=None) + return parsed + return None + + +def _coerce_date_value(value): + if isinstance(value, datetime): + return value.date() + if isinstance(value, date): + return value + parsed = _parse_calendar_datetime(value) + if parsed: + return parsed.date() + if isinstance(value, str): + stripped = value.strip() + for fmt in ("%Y-%m-%d", "%m/%d/%Y"): + try: + return datetime.strptime(stripped, fmt).date() + except ValueError: + continue + return None + + +def _coerce_time_value(value, date_hint): + parsed = _parse_calendar_datetime(value) + if parsed: + return parsed + date_hint = date_hint or datetime.now().date() + if isinstance(value, time): + return datetime.combine(date_hint, value) + if isinstance(value, str): + stripped = value.strip() + for fmt in ("%H:%M:%S", "%H:%M"): + try: + t = datetime.strptime(stripped, fmt).time() + return datetime.combine(date_hint, t) + except ValueError: + continue + try: + parsed = datetime.fromisoformat(stripped) + return parsed.replace( + year=date_hint.year, + month=date_hint.month, + day=date_hint.day, + ) + except ValueError: + pass + if isinstance(value, (int, float)): + return datetime.fromtimestamp(value) + return None + + +def _extract_date_click_payload(payload): + if not payload: + return None, True + if isinstance(payload, dict): + date_value = payload.get("dateStr") or payload.get("date") + view = payload.get("view") or {} + view_type = view.get("type") + all_day = payload.get("allDay") + if all_day is None and view_type: + all_day = view_type.startswith("dayGrid") or view_type.startswith("multiMonth") + return date_value, bool(all_day) if all_day is not None else True + return payload, True + + +def _build_modal_response(start_dt, end_dt, *, all_day): + start_dt = start_dt or datetime.now() + if all_day: + if not end_dt: + end_dt = start_dt + timedelta(days=1) + if end_dt <= start_dt: + end_dt = start_dt + timedelta(days=1) + # display end date inclusive + end_display = end_dt - timedelta(days=1) if end_dt.date() > start_dt.date() else start_dt + return ( + True, + start_dt.strftime("%Y-%m-%d"), + end_display.strftime("%Y-%m-%d"), + None, + None, + {"allDay": True}, + ) + + end_dt = end_dt or (start_dt + timedelta(hours=1)) + if end_dt <= start_dt: + end_dt = start_dt + timedelta(hours=1) + return ( + True, + start_dt.strftime("%Y-%m-%d"), + end_dt.strftime("%Y-%m-%d"), + start_dt.strftime("%H:%M:%S"), + end_dt.strftime("%H:%M:%S"), + {"allDay": False}, + ) + + component = html.Div( [ - fcc.FullCalendarComponent( - id="calendar", # Unique ID for the component - initialView="dayGridMonth", # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "listWeek,timeGridDay,timeGridWeek,dayGridMonth", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=[], - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates + dcc.Store(id="new_event_selection_meta", data={"allDay": False}), + html.Div( + dcal.FullCalendar( + id="calendar", # Unique ID for the component + initialView="dayGridMonth", # dayGridMonth, timeGridWeek, timeGridDay, listWeek, + # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek + headerToolbar={ + "left": "prev,next today", + "center": "", + "right": "listWeek,timeGridDay,timeGridWeek,dayGridMonth", + }, # Calendar header + initialDate=f"{formatted_date}", # Start date for calendar + editable=True, # Allow events to be edited + selectable=True, # Allow dates to be selected + events=[], + nowIndicator=True, # Show current time indicator + navLinks=True, # Allow navigation to other dates + ), + className="dark-calendar", + style=CALENDAR_STYLE, ), dmc.MantineProvider( theme={"colorScheme": "dark"}, @@ -250,6 +389,7 @@ ], ), ], + style={"padding": "1.5rem 0"}, ) @@ -258,11 +398,11 @@ Output("modal", "title"), Output("modal_event_display_context", "children"), Input("modal-close-button", "n_clicks"), - Input("calendar", "clickedEvent"), + Input("calendar", "eventClick"), State("modal", "opened"), prevent_initial_call=True # Set this to True ) -def open_event_modal(n, clickedEvent, opened): +def open_event_modal(n, event_click, opened): ctx = callback_context @@ -271,9 +411,10 @@ def open_event_modal(n, clickedEvent, opened): else: button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == "calendar" and clickedEvent is not None: - event_title = clickedEvent["title"] - event_context = clickedEvent["extendedProps"]["context"] + if button_id == "calendar" and event_click is not None: + event_title = event_click.get("title", "Selected event") + extended_props = event_click.get("extendedProps") or {} + event_context = extended_props.get("context", "No additional details provided.") return ( True, event_title, @@ -292,9 +433,9 @@ def open_event_modal(n, clickedEvent, opened): ), ) elif button_id == "modal-close-button" and n is not None: - return False, dash.no_update, dash.no_update + return False, no_update, no_update - return opened, dash.no_update + return opened, no_update, no_update @callback( @@ -303,38 +444,42 @@ def open_event_modal(n, clickedEvent, opened): Output("end_date", "value"), Output("start_time", "value"), Output("end_time", "value"), - Input("calendar", "dateClicked"), + Output("new_event_selection_meta", "data"), + Input("calendar", "dateClick"), + Input("calendar", "select"), Input("modal_close_new_event_button", "n_clicks"), State("add_modal", "opened"), ) -def open_add_modal(dateClicked, close_clicks, opened): +def open_add_modal(date_clicked, date_selected, close_clicks, opened): ctx = callback_context if not ctx.triggered: raise PreventUpdate - else: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == "calendar" and dateClicked is not None: - try: - start_time = datetime.strptime(dateClicked, "%Y-%m-%dT%H:%M:%S%z").time() - start_date_obj = datetime.strptime(dateClicked, "%Y-%m-%dT%H:%M:%S%z") - start_date = start_date_obj.strftime("%Y-%m-%d") - end_date = start_date_obj.strftime("%Y-%m-%d") - except ValueError: - start_time = datetime.now().time() - start_date_obj = datetime.strptime(dateClicked, "%Y-%m-%d") - start_date = start_date_obj.strftime("%Y-%m-%d") - end_date = start_date_obj.strftime("%Y-%m-%d") - end_time = datetime.combine(date.today(), start_time) + timedelta(hours=1) - start_time_str = start_time.strftime("%Y-%m-%d %H:%M:%S") - end_time_str = end_time.strftime("%Y-%m-%d %H:%M:%S") - return True, start_date, end_date, start_time_str, end_time_str + trigger = ctx.triggered[0]["prop_id"] + + if trigger == "calendar.dateClick" and date_clicked is not None: + date_value, is_all_day = _extract_date_click_payload(date_clicked) + start_dt = _parse_calendar_datetime(date_value) or datetime.now() + end_dt = start_dt + (timedelta(days=1) if is_all_day else timedelta(hours=1)) + return _build_modal_response(start_dt, end_dt, all_day=is_all_day) + + if trigger == "calendar.select" and date_selected: + selection = date_selected or {} + start_dt = _parse_calendar_datetime(selection.get("start")) or datetime.now() + end_dt = _parse_calendar_datetime(selection.get("end")) - elif button_id == "modal_close_new_event_button" and close_clicks is not None: - return False, dash.no_update, dash.no_update, dash.no_update, dash.no_update + if not end_dt: + end_dt = start_dt + timedelta(hours=1) + is_all_day = bool(selection.get("allDay")) + if is_all_day and end_dt <= start_dt: + end_dt = start_dt + timedelta(days=1) + return _build_modal_response(start_dt, end_dt, all_day=is_all_day) - return opened, dash.no_update, dash.no_update, dash.no_update, dash.no_update + if trigger == "modal_close_new_event_button.n_clicks" and close_clicks is not None: + return False, no_update, no_update, no_update, no_update, {"allDay": False} + + return opened, no_update, no_update, no_update, no_update, {"allDay": False} @callback( @@ -352,6 +497,7 @@ def open_add_modal(dateClicked, close_clicks, opened): State("event_color_select", "value"), State("rich_text_output", "children"), State("calendar", "events"), + State("new_event_selection_meta", "data"), prevent_initial_call=True # Set this to True ) @@ -365,28 +511,53 @@ def add_new_event( event_color, event_context, current_events, + selection_meta, ): if n is None: raise PreventUpdate - start_time_obj = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") - end_time_obj = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") + selection_meta = selection_meta or {} + wants_all_day = bool(selection_meta.get("allDay")) + has_time_inputs = bool(start_time or end_time) + use_all_day = wants_all_day and not has_time_inputs + + safe_title = event_name or "Untitled event" + safe_color = event_color or "bg-gradient-primary" + safe_context = event_context or "" + events = current_events or [] + + start_date_obj = _coerce_date_value(start_date) or datetime.now().date() + end_date_obj = _coerce_date_value(end_date) or start_date_obj + if end_date_obj < start_date_obj: + end_date_obj = start_date_obj - start_time_str = start_time_obj.strftime("%H:%M:%S") - end_time_str = end_time_obj.strftime("%H:%M:%S") + if use_all_day: + new_event = { + "title": safe_title, + "start": start_date_obj.isoformat(), + "end": (end_date_obj + timedelta(days=1)).isoformat(), + "allDay": True, + "className": safe_color, + "extendedProps": {"context": safe_context}, + } + else: + start_dt = _coerce_time_value(start_time, start_date_obj) or datetime.combine( + start_date_obj, datetime.min.time() + ) + end_dt = _coerce_time_value(end_time, end_date_obj) or (start_dt + timedelta(hours=1)) - start_date = f"{start_date}T{start_time_str}" - end_date = f"{end_date}T{end_time_str}" + if end_dt <= start_dt: + end_dt = start_dt + timedelta(hours=1) - new_event = { - "title": event_name, - "start": start_date, - "end": end_date, - "className": event_color, - "context": event_context, - } + new_event = { + "title": safe_title, + "start": start_dt.isoformat(), + "end": end_dt.isoformat(), + "className": safe_color, + "extendedProps": {"context": safe_context}, + } - return current_events + [new_event], False, "", "bg-gradient-primary", "" + return events + [new_event], False, "", "bg-gradient-primary", "" @callback( @@ -396,7 +567,3 @@ def add_new_event( ) def display_output(value, charCount): return value - - - - diff --git a/docs/full_calendar_component/section_renders.py b/docs/full_calendar_component/section_renders.py index c8174e6..d429603 100644 --- a/docs/full_calendar_component/section_renders.py +++ b/docs/full_calendar_component/section_renders.py @@ -1,247 +1,141 @@ -from typing import Literal +from datetime import datetime, timedelta -import dash +import dash_fullcalendar as dcal import dash_mantine_components as dmc -from dash import * -import full_calendar_component as fcc -from datetime import datetime, date, timedelta -from dash.exceptions import PreventUpdate +from dash import Input, Output, callback, html + +CALENDAR_STYLE = { + "--fc-page-bg-color": "#101113", + "--fc-neutral-bg-color": "#1a1b1e", + "--fc-neutral-text-color": "#f1f3f5", + "--fc-border-color": "#2c2e33", + "--fc-button-text-color": "#f1f3f5", + "--fc-button-bg-color": "#2c2e33", + "--fc-button-border-color": "#373a40", + "--fc-event-text-color": "#f8f9fa", +} + +BASE_VIEWS = [ + ("dayGridMonth", "Monthly grid (dayGridMonth)"), + ("timeGridWeek", "Weekly schedule (timeGridWeek)"), + ("timeGridDay", "Single day (timeGridDay)"), + ("listWeek", "Agenda list (listWeek)"), + ("listDay", "Agenda day (listDay)"), + ("multiMonthYear", "Multi-month year (multiMonthYear)"), +] + +HAS_RESOURCE_API = "resources" in getattr(dcal.FullCalendar, "available_properties", []) + +PREMIUM_VIEW_OPTIONS = [ + ("resourceTimelineWeek", "Resource timeline week*"), + ("resourceTimeGridDay", "Resource time grid day*"), +] + +VIEW_OPTIONS = BASE_VIEWS + (PREMIUM_VIEW_OPTIONS if HAS_RESOURCE_API else []) +PREMIUM_VIEWS = {value for value, _ in PREMIUM_VIEW_OPTIONS} if HAS_RESOURCE_API else set() + +RESOURCES = [ + {"id": "room-1", "title": "Room 101"}, + {"id": "room-2", "title": "Room 202"}, + {"id": "hybrid", "title": "Remote Crew"}, +] + + +def _build_events(): + base_day = datetime.now().date() + + def slot(title, day_offset, hour, duration, resource, color, context): + start = datetime.combine(base_day + timedelta(days=day_offset), datetime.min.time()) + timedelta(hours=hour) + end = start + timedelta(hours=duration) + event = { + "id": title.lower().replace(" ", "-"), + "title": title, + "start": start.strftime("%Y-%m-%dT%H:%M:%S"), + "end": end.strftime("%Y-%m-%dT%H:%M:%S"), + "className": color, + "extendedProps": {"context": context}, + } + if resource: + event["resourceId"] = resource + return event + + return [ + slot("Product Kickoff", 0, 9, 1.5, "room-1", "bg-gradient-success", "Launch plan and scope review."), + slot("Design Review", 1, 11, 1, "room-2", "bg-gradient-info", "UX polish and acceptance."), + slot("Customer Sync", 2, 14, 1, "hybrid", "bg-gradient-warning", "Weekly touchpoint with enterprise clients."), + slot("Deployment Window", 3, 21, 2, "room-1", "bg-gradient-danger", "Late-night release window."), + slot("Sprint Demo", 4, 13, 1.5, "room-2", "bg-gradient-primary", "Showcase progress to stakeholders."), + ] component = dmc.SimpleGrid( cols={"base": 1, "sm": 1, "lg": 4}, + spacing="2rem", children=[ dmc.Paper( html.Div(id="view-fcc"), id="intro-wrapper-fcc", style={"gridColumn": "1 / 4"}, + withBorder=True, + radius="md", + p="md", ), dmc.Stack( [ dmc.RadioGroup( - label="initialView render", + label="Choose the initial view (premium views marked with *)", id="intro-view-fcc", - value="dayGridMonth", # Set the default value to text-1 + value="dayGridMonth", children=dmc.Stack( - [dmc.Radio(label=x, value=x) for x in ["dayGridMonth", "timeGridWeek", "timeGridDay", "listWeek", "dayGridWeek", "dayGridYear", "multiMonthYear", "resourceTimeline", "resourceTimeGridDay"]], - gap="0.5rem", + [dmc.Radio(label=label, value=value) for value, label in VIEW_OPTIONS], + gap="0.4rem", ), ), - ], + dmc.Text( + "Resource views load automatically when Scheduler plugins and license keys are available." + if HAS_RESOURCE_API + else "Install the latest dash-fullcalendar (Scheduler build) to try the resource views.", + size="sm", + c="dimmed", + ), + ] ), ], - spacing="2rem", + style={"padding": "1.5rem 0"}, ) -@callback( - Output("view-fcc", "children"), - # Output('test-out', 'children'), - Input("intro-view-fcc", "value"), -) +@callback(Output("view-fcc", "children"), Input("intro-view-fcc", "value")) def update_form(render: str): - # Get today's date - today = datetime.now() - - # Format the date - formatted_date = today.strftime("%Y-%m-%d") - - events = [ - { - "title": "Pip Install Python", - "start": f"{formatted_date}", - "end": f"{formatted_date}", - "className": "bg-gradient-success", - "context": "Pip Install FullCalendar", - }, - { - 'title': 'Meeting with the boss', - 'start': f"{formatted_date}T14:30:00", - 'end': f"{formatted_date}T15:30:00", - 'className': 'bg-gradient-info', - 'context': 'Meeting with the boss', - }, - { - 'title': 'Happy Hour', - 'start': f"{formatted_date}T17:30:00", - 'end': f"{formatted_date}T18:30:00", - 'className': 'bg-gradient-warning', - 'context': 'Happy Hour', + events = _build_events() + calendar_id = f"view-calendar-preview-{render}" + calendar_kwargs = { + "id": calendar_id, + "initialView": render, + "initialDate": events[0]["start"].split("T")[0], + "headerToolbar": { + "left": "prev,next today", + "center": "title", + "right": "dayGridMonth,timeGridWeek,timeGridDay,listWeek,listDay,multiMonthYear", }, - { - 'title': 'Dinner', - 'start': f"{formatted_date}T20:00:00", - 'end': f"{formatted_date}T21:00:00", - 'className': 'bg-gradient-danger', - 'context': 'Dinner', - } - ] - if render == 'dayGridMonth': - return fcc.FullCalendarComponent( - id="view-calendar1", # Unique ID for the component - initialView='dayGridMonth', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'timeGridWeek': - return fcc.FullCalendarComponent( - id="view-calendar2", # Unique ID for the component - initialView='timeGridWeek', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'timeGridDay': - return fcc.FullCalendarComponent( - id="view-calendar3", # Unique ID for the component - initialView='timeGridDay', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'listWeek': - return fcc.FullCalendarComponent( - id="view-calendar4", # Unique ID for the component - initialView='listWeek', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'dayGridWeek': - return fcc.FullCalendarComponent( - id="view-calendar5", # Unique ID for the component - initialView='dayGridWeek', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'dayGridYear': - return fcc.FullCalendarComponent( - id="view-calendar6", # Unique ID for the component - initialView='dayGridYear', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'multiMonthYear': - return fcc.FullCalendarComponent( - id="view-calendar7", # Unique ID for the component - initialView='multiMonthYear', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'resourceTimeline': - return fcc.FullCalendarComponent( - id="view-calendar8", # Unique ID for the component - initialView='resourceTimeline', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) - elif render == 'resourceTimeGridDay': - return fcc.FullCalendarComponent( - id="view-calendar9", # Unique ID for the component - initialView='resourceTimeGridDay', # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates + "events": events, + "navLinks": True, + "weekends": True, + "nowIndicator": True, + "height": "750px", + } + + if render in PREMIUM_VIEWS and HAS_RESOURCE_API: + calendar_kwargs.update( + { + "schedulerLicenseKey": "GPL-My-Project-Is-Open-Source", + "plugins": ["resourceTimeline", "resourceTimeGrid", "resource"], + "resources": RESOURCES, + } ) - return fcc.FullCalendarComponent( - id="view-calendar", # Unique ID for the component - initialView=f"{render}", # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=events, - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ) \ No newline at end of file + + return html.Div( + dcal.FullCalendar(**calendar_kwargs), + className="dark-calendar", + style=CALENDAR_STYLE, + ) diff --git a/pages/home.md b/pages/home.md index 9618a64..4354d3d 100644 --- a/pages/home.md +++ b/pages/home.md @@ -18,7 +18,7 @@ These are the components that I have built and am currently maintaining: * [![Downloads](https://static.pepy.tech/badge/dash-summernote)](https://pepy.tech/project/dash-summernote) `Dash Summernote` - A rich text WYSIWYG Editor for Dash * [![Downloads](https://static.pepy.tech/badge/dash-insta-stories)](https://pepy.tech/project/dash-insta-stories) `Dash Insta Stories` - A Instagram Stories Component for Dash * [![Downloads](https://static.pepy.tech/badge/dash-image-gallery)](https://pepy.tech/project/dash-image-gallery) `Dash Image Gallery` - A Image Gallery Component for Dash -* [![Downloads](https://static.pepy.tech/badge/full-calendar-component)](https://pepy.tech/project/full-calendar-component) `Full-Calendar-Component` - A Full Calendar Component for Dash +* [![Downloads](https://static.pepy.tech/badge/dash-fullcalendar)](https://pepy.tech/project/dash-fullcalendar) `Dash FullCalendar` - A thin Dash wrapper around FullCalendar * [![Downloads](https://static.pepy.tech/badge/dash-gauge)](https://pepy.tech/project/dash-gauge) `Dash Gauge` - A Gauge Component for Dash * [![Downloads](https://static.pepy.tech/badge/dash-emoji-mart)](https://pepy.tech/project/dash-emoji-mart) `Dash Emoji Mart` - A Slack-like Emoji Picker for Dash * [![Downloads](https://static.pepy.tech/badge/dash-dynamic-grid-layout)](https://pepy.tech/project/dash-dynamic-grid-layout) `Dash Dynamic Grid Layout` - A Dynamic Grid Layout Component for Dash @@ -36,4 +36,3 @@ These are the components that I have built and am currently maintaining: [//]: # ([![Dash Dock Preivew](/assets/images/about_me.png)](https://youtu.be/9Bcw_RmTQ2o?si=hsExmOB3436wZG6y)) - diff --git a/requirements.txt b/requirements.txt index da10ac5..8d6f2f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,9 +26,9 @@ dash-iconify colorlover statsmodels python-frontmatter>=1.1.0 -git+https://github.com/AnnMarieW/markdown2dash-amw.git#egg=markdown2dash pydantic -full-calendar-component +git+https://github.com/AnnMarieW/markdown2dash-amw.git#egg=markdown2dash +dash-fullcalendar>=0.1.0 dash-dock>=0.0.1 dash-discord>=0.0.3 dash-summernote @@ -51,4 +51,4 @@ dash_quill dash-ag-grid python-dotenv==1.0.1 dash-ace==0.2.1 -gunicorn>=21.2.0 \ No newline at end of file +gunicorn>=21.2.0 diff --git a/runners/full_calendar/__pycache__/run.cpython-313.pyc b/runners/full_calendar/__pycache__/run.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f43e2a008eeef0cf0dfa3b2fdb228af0c63486a3 GIT binary patch literal 4679 zcmcgvTW=f36`tji%iEIF)w-Crmy9HvX-!H_oLITpsx4chWJ%#FXx}w zb}3sd;L1&bG|ofZ7C}{@K$E8=FGcfI|A77g(F!1Dq5=V0pvW6#w^WjS*jn> zqAO^2=FH5Qv*$bKo5M$uNC?3*VaK#TMG^Wd9kd=8yU!z?Md$-0B9WOvn7%zTOxA-v z0`0RiY}SjtSs(UgIm~7K*q;sHKsJbj?mROS!r{CpiSqnerlBx*w5fT|>uj@+qxop= z_@0TDyd&RnnEWsq?#y?=d7Z8E@+dcQP~*#Bjg&;_@Y!7I;0_V(*L&8}SR=q$qW5$d zA>5k}=DvGyXA+6NmNf#(Ad!2W!F}-d!@EE41#1VyK)yfUmk;NjKe#{l!a><-5V`jB zwls@DF*MHMIG_@qIhH++k7rNd6IlTZ*#u5xlQ`*G%2P{oR}N}yV=yE}1`!?>qtK3k zMRNz&g2mz7!l811E_aw*jENoN4D4DwY!>8i9wrw%#V)a1>=|eOo#z0@qvDAc?s*@K zW1`R+x87o65Vd`o0c4Xefh|%O;1$nFkM*^BVnBRG91@4eSph9G%S>wIYdSTTVu*J^ za*8RAcrQtIk@%(!tLzYNzUUUyTXl~OP@)P zru>!DMaQx1mok~MVXZ8uWwVsIP?8KsGt`VCIjVi(#f8hQUZM%|w%5u3VVdshU zqfc8eVY6(j`u-EnK4phtI@*#ZOO9q5PaHJ%v~3nPm$7O;vE_1Wi>w&w8@8hA+A2;P zs*^FSQpPTqN-BmE)eEEPbLp{j8AY?5J>qmpGhCm%*wO|- z5iBwk2@7J;yeWgrQa%#5%a%ofD}cTFbxB?+sJCR*a`7h}>MaKllJtTtW6g4G((_p2 zhHO!VU?-7A&=;0W9kPJ<9L>?G_ON7Wpie4Ux=MK1ilxGmrs|4KqD57LwFSpC;aE&~ z&2X@(lx2D{iP@^#Y37w(iCEGyKwPAtmlU~`!1V2hf4C>nqd!3N0lEflLFfRtEj|dt)=`xOeL8FFVYMFdI2OBDH6mdT9T|{>M0VqD&ZByylD{s)cn=S!qrQk zqDRz7631l&Hrbga2o@R-ZnQuQ8)<8egc}rBpb+3^Vg1YSZ(oLH9qojp@4tHY)xYtv z_owepZyq^&|Me<=ey1_q@Vq-uyR&y^f7iFgC+dE0FjNnrXjd&9+l>Wyu8sme4xa*5 z`+)5LfeY*dC>Z{~z>3_y0BC~@7BwFT6bd49tqpsw4Rvb`+8b<#>j=6!S_L2aRysni z4(cuU`Bpl@t`4{;{|gIx@T%5u;+v4gQWf4D}GgeYEL^rm}EVGqjSV3ynw= z4l6B7R=ZB+vUFm)ISLRrvAdE84E zcGHH!fjen)k~TC!Fs+WsKr7sX9+v~z5`1)_+8B4SF^H-tmS;EOcso-&n4e^we6ifU!$~IIy zYlK~vEG$sHJdLo^H1uFK3CpH#Vo@%tV22lO&lMUm5REVl$VG@Dz=8xUs1^;Wq{={5 z?T2!=OJMXz#0TgNxLe$M6e#0bdtIwu9YWXWTK9P@OL^w-Igq71F0wB1rWiZ{BAcoZ z8UfKFXKFgWNGIt(4a9Zyl|Q!GJ+|c^+ligpiaom-cy=dzr>NJ<%iwdh)RDwGQs-^k@A=N1c>r)MV#XFa3YVsHB$%6Am@nk!=NG{!2ZM!R6yY?(RxtoKA&QfWVqsxoaf);mpeTo8 z0&>5w3RfJhF~W_ayHUaTTLlhSLU9C3xW+tTCuU~}D_Ita(+o@RPr7z=H4@cLSvPrU zTD!SSf^e=E2)|~@<|0m5Ak8Osp{=tF!+eQCe?k3!N9VWE z`7hD%ZUA-k+?#su_I7Nz8XMkl%dp&EZ=n?F|Nk2P|AJ@^I7t*^cP z+D7;9`+wWN#U^TA|2tE6rrw#qGksKCl&(vAu9dwiT-x(olg#Z8m literal 0 HcmV?d00001 diff --git a/runners/full_calendar/requirements.txt b/runners/full_calendar/requirements.txt new file mode 100644 index 0000000..1cdecb4 --- /dev/null +++ b/runners/full_calendar/requirements.txt @@ -0,0 +1,6 @@ +dash==2.18.2 +dash-fullcalendar>=0.1.0 +dash-mantine-components==0.15.2 +dash-quill +requests +git+https://github.com/AnnMarieW/markdown2dash-amw.git#egg=markdown2dash diff --git a/runners/full_calendar/run.py b/runners/full_calendar/run.py new file mode 100644 index 0000000..2e78264 --- /dev/null +++ b/runners/full_calendar/run.py @@ -0,0 +1,123 @@ +import sys +from pathlib import Path + +from dash import Dash, Input, Output, dcc, html, _dash_renderer +import dash_mantine_components as dmc + +# Ensure the repo root is on sys.path so docs/* modules import cleanly when this +# runner is executed from its own directory. +CURRENT_FILE = Path(__file__).resolve() +RUNNER_DIR = CURRENT_FILE.parent +REPO_ROOT = RUNNER_DIR.parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +# Use the shared assets directory (dark theme CSS, icons, etc.). +ASSETS_PATH = REPO_ROOT / "assets" + +# Set React 18 so dash-mantine-components loads correctly +_dash_renderer._set_react_version("18.2.0") + +stylesheets = [ + "https://unpkg.com/@mantine/dates@7/styles.css", + "https://unpkg.com/@mantine/charts@7/styles.css", + "https://unpkg.com/@mantine/carousel@7/styles.css", + "https://unpkg.com/@mantine/notifications@7/styles.css", + "https://unpkg.com/@mantine/nprogress@7/styles.css", + "https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css", + "https://use.fontawesome.com/releases/v6.2.1/css/all.css", +] + +scripts = [ + "https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.8/dayjs.min.js", + "https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.8/locale/ru.min.js", + "https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.8/locale/fr.min.js", + "https://unpkg.com/hotkeys-js/dist/hotkeys.min.js", +] + +app = Dash( + __name__, + suppress_callback_exceptions=True, + external_scripts=scripts, + external_stylesheets=stylesheets + dmc.styles.ALL, + assets_folder=str(ASSETS_PATH), + title="Dash FullCalendar Showcase", +) + +# Import the interactive examples after the Dash app exists so callbacks attach cleanly. +from docs.full_calendar_component import ( # noqa: E402 + api_example, + extra_fields, + header_toolbar, + introduction, + section_renders, +) + +HOME_MD = (REPO_ROOT / "pages/home.md").read_text() + + +def render_home(): + return dmc.Container( + [ + dmc.Title("Pip Install Python Components", order=2), + dmc.Space(h=10), + dcc.Markdown(HOME_MD), + ], + size="lg", + py="xl", + ) + + +def render_full_calendar_docs(): + return dmc.Container( + [ + dmc.Title("Dash FullCalendar Showcase", order=2), + dmc.Text( + "Minimal runner containing the home page and the Dash FullCalendar examples only.", + c="dimmed", + ), + dmc.Divider(label="Interactive Builder"), + introduction.component, + dmc.Divider(label="Views & Layouts"), + section_renders.component, + dmc.Divider(label="Header Toolbar"), + header_toolbar.component, + dmc.Divider(label="Advanced Workflow"), + extra_fields.component, + dmc.Divider(label="API-driven Calendar"), + api_example.component, + ], + size="lg", + py="xl", + ) + + +app.layout = dmc.MantineProvider( + dmc.Stack( + [ + dcc.Location(id="url"), + dmc.Group( + [ + dmc.Anchor("Home", href="/", size="lg"), + dmc.Anchor("Full Calendar Docs", href="/pip/full_calendar_component", size="lg"), + ], + justify="center", + gap="xl", + py="md", + ), + html.Div(id="page-content"), + ] + ), + theme={"colorScheme": "dark"}, +) + + +@app.callback(Output("page-content", "children"), Input("url", "pathname")) +def render_page(pathname): + if pathname == "/pip/full_calendar_component": + return render_full_calendar_docs() + return render_home() + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=8059)