{#- A date field. It is text, not , so it reads in the date format chosen in Settings (the native one follows the browser's language); static/dates.js adds a calendar button. `value` is an ISO date, a date, or whatever the person typed (kept as typed after an error). -#} {% macro date_input(name, value='', id='', label='', title='') -%} {%- endmacro %} {% macro datetime_input(name, value='') -%} {%- endmacro %} {# Shared form fragments. #} {% macro err(errors, name) %} {%- if errors and name in errors %}{{ errors[name] }}{% endif -%} {% endmacro %} {% macro errors_box(errors) %} {%- if errors %}
Could not save:
{%- endif %} {% endmacro %} {% macro filter_control(col, active, form_id) %} {%- if col.kind == 'enum' -%} {%- else -%} {%- endif -%} {% endmacro %} {% macro sort_links(col, sort, dir, sort_url) %} ▲▼ {%- endmacro %} {% macro filter_help() %} ?what does !text mean? Filters (one box per column, combined with AND)
textcontains
!textdoes not contain
=textequals
>5 <5larger / smaller (numbers, dates as {{ date_hint() }}, otherwise text order)
-empty
*not empty
Lists: cmd-click (ctrl-click) to select several. Sorting: ▲ A to Z, ▼ Z to A next to each heading; empty values sort last. Filters, sorting and search stay in the URL, so a view can be bookmarked.
{%- endmacro %} {# A "?" that explains on hover, focus or tap: the detail a page needs once, so the line next to it can stay short. `id` is unique on the page (the "?" is described by it); `label` names the "?" for a screen reader. The text is the call block: {% call hint('some-id', 'What this means') %}Text, links and bold.{% endcall %} Keep it out of a .table-scroll, which would clip the tip. #} {% macro hint(id, label='More detail') %} {{ caller() }} {%- endmacro %} {% macro filter_form(form_id, action, active, q=None, sort='', dir='asc') %}
{% if q is not none %}{% endif %} {% if sort %}{% endif %} {{ filter_help() }} {% if active or sort %}clear filters and sorting{% endif %}
{% endmacro %} {# An interaction is with a person. With `contact` (the contact page) the person is fixed; elsewhere you pick one. A note has no direction and may be about the whole company instead (contact left empty). #} {% macro interaction_form(action, company, values, errors=None, submit='Log interaction', contact=None) %}
{{ errors_box(errors) }}
{% for d in directions %} {% endfor %} {% if contact %} {% else %} {% endif %}
{{ custom_fields(custom_fields_for('interaction'), values, errors) }} {# The body is the record. Custom fields sit beside it and never touch it. #}
{% endmacro %} {# The labels of the user's own fields. `grid`: they sit in a .form-grid (the company form), three columns each, instead of in a .row of their own. #} {% macro custom_labels(defs, values, errors, grid=false) %} {% for f in defs %} {{ f.label }} {%- if f.type == 'select' %} {%- elif f.type == 'date' %} {{ date_input('custom_' ~ f.key, values.get('custom_' ~ f.key, '')) }} {%- elif f.type == 'number' %} {%- else %} {%- endif %} {{ err(errors, 'custom_' ~ f.key) }} {%- if f.help %} {{ f.help }}{% endif %} {% endfor %} {% endmacro %} {% macro custom_fields(defs, values, errors) %} {%- if defs %}
{{ custom_labels(defs, values, errors) }}
{%- endif %} {% endmacro %} {# `hide`: the fields layout.toml hides on the record page. A form leaves them out entirely, so saving keeps their values (web.submitted_only). #} {# The company form: groups of fields on a 12-column grid (.form-grid, the span-N classes in style.css). `hide` is what layout.toml hides: such a field is left out, and a group with nothing left in it is left out with it. The reason and requalify fields only show for a stage that keeps them, or when they already hold something; they are hidden, not removed, so they still submit. #} {#- `first_note`: the new-company form, where one box becomes the company's first note. The edit form has none: company text lives in dated notes on the page (#company-notes). #} {% macro company_fields(values, errors, first_note=false) %} {%- set hide = layout_hidden('company', errors) %} {%- set custom = custom_fields_for('company') | rejectattr('key', 'in', hide) | list %} {%- set keeps = stage_keeps(values.stage) %} {#- which stages keep a reason (and a date to requalify on), for the script below #} {%- set keeps_by_stage = {} %} {%- for s in stages if stage_keeps(s) %}{% set _ = keeps_by_stage.update({s: stage_keeps(s)}) %}{% endfor %} {%- set show_type = type_names() and 'next_step_type' not in hide %}
{#- The name is not a login: tell password managers to leave it alone. #} {% if 'source' not in hide %}{% endif %} {% if 'website' not in hide %}{% endif %} {% if 'linkedin' not in hide %}{% endif %} {% if 'country' not in hide %}{% endif %}
{% if 'lost_reason' not in hide or 'requalify_on' not in hide %} {%- set reason_shown = 'lost_reason' not in hide and (keeps or values.lost_reason or errors.lost_reason) %} {%- set when_shown = 'requalify_on' not in hide and ('requalify' in keeps or values.requalify_on or errors.requalify_on) %}
Closing details
{% if 'lost_reason' not in hide %}{% endif %} {% if 'requalify_on' not in hide %}{% endif %}
{% endif %} {% if 'product_oneliner' not in hide or 'value_eur_month' not in hide or custom %}
Profile
{% if 'product_oneliner' not in hide %}{% endif %} {% if 'value_eur_month' not in hide %}{% endif %} {{ custom_labels(custom, values, errors, grid=true) }}
{% endif %} {% if 'next_step' not in hide or 'next_step_due' not in hide or 'next_step_status' not in hide or show_type %}
Next step
{% if 'next_step' not in hide %}{% endif %} {% if 'next_step_due' not in hide %}{% endif %} {% if 'next_step_status' not in hide %}{% endif %} {% if show_type %}{% endif %}
{% endif %} {% if 'tags' not in hide or first_note %}
{{ 'Tags and first note' if first_note and 'tags' not in hide else ('First note' if first_note else 'Tags') }}
{% if 'tags' not in hide %}{% endif %} {% if first_note %}{% endif %} {% if not first_note %}

Free text about the company goes in Company notes, as dated notes.

{% endif %}
{% endif %} {% endmacro %} {% macro contact_fields(values, errors) %} {%- set hide = layout_hidden('contact', errors) %}
{% if 'title' not in hide %}{% endif %} {% if 'role' not in hide %}{% endif %} {% set lang = (values.language or '') | trim | lower %} {% if 'language' not in hide %}{% endif %}
{% if 'email' not in hide or 'phone' not in hide or 'linkedin' not in hide %}
{% if 'email' not in hide %}{% endif %} {% if 'phone' not in hide %}{% endif %} {% if 'linkedin' not in hide %}{% endif %}
{% endif %} {{ custom_fields(custom_fields_for('contact') | rejectattr('key', 'in', hide) | list, values, errors) }} {% endmacro %} {# `contact_page`: every entry is with the same person, so no name; the direction is an arrow, a missing subject says nothing, and a message shows its first line with the rest behind "more". #} {% macro timeline(company, interactions, contact_page=false) %} {% endmacro %} {# One group of someone else's notes on a contact page: a one-line summary (who, how many, the newest note's first line) that opens to the newest ones. `group.total` can exceed what is listed; `href` is where all of them are, `where` what its link calls that place (default: the label). #} {% macro note_group(label, group, href='', where='') %} {% set notes = group.notes %}
{{ label }} ({{ group.total }}) {{ preview(notes[0].body) }} more {% if group.total > notes | length %}

Showing the newest {{ notes | length }} of {{ group.total }}.

{% endif %} {% if href %}

{{ 'Open ' ~ (where or label) ~ (' to see all' if group.total > notes | length else '') }}

{% endif %}
{% endmacro %} {% macro type_select(current='', autosubmit=false, name='type') -%} {%- set options = type_names() -%} {%- if options -%} {%- endif -%} {%- endmacro %} {% macro type_cell(company, t, index, contact='') -%} {% if type_names() and not t.done %}
{{ type_select(t.type, autosubmit=true) }}
{% else %}{{ type_chip(t.type) }}{% endif %} {%- endmacro %} {% macro type_chip(name) -%} {%- if name -%} {%- set colour = type_colour(name) -%} {{ name }} {%- endif -%} {%- endmacro %} {% macro todo_actions(company, todo, back='', delete=false) -%} {%- if todo.is_field -%}
{% if back %}{% endif %}
{%- if delete %}
{% if back %}{% endif %}
{%- endif %} {%- else -%}
{% if back %}{% endif %}
{%- if delete %}
{% if back %}{% endif %}
{%- endif %} {%- endif -%} {%- endmacro %} {# Edit the text and due date of one open to-do. `index` is None for the company's next-step fields; `contact` is the person's slug or ''. #} {% macro todo_edit(company, t, index, contact='', back='') -%} {%- if not t.done -%}
Edit
{% if back %}{% endif %} {{ date_input('due', t.due, label='due') }}
{%- endif -%} {%- endmacro %} {% macro task_section(company, today) -%} {% set first = company.next_todo() %}

Tasks

For the company and its people. {%- call hint('company-next-step-help', 'What the next step is') %} The next step is the open task due first. Change a due date to change what is next.{% endcall %} This company on the Tasks page

{% if type_names() %}{% endif %} {% for t in company.open_todos() + (company.todos() | selectattr('done') | list) %} {% set is_next = first and not first.done and not t.done and t.key == first.key %} {% if type_names() %}{{ type_cell(company, t, t.index, t.contact.slug if t.contact else '') }}{% endif %} {% else %} {% endfor %}
tasktypeforduestatus
{{ t.text or '(no text)' }}{% if is_next %} next step{% endif %}{% if not type_names() %} {{ type_chip(t.type) }}{% endif %}{% if t.contact %}{{ t.contact.name }}{% else %}company{% endif %} {{ fmt_date(t.due) if t.due else 'no date' }} {% if t.done %}done{% if t.done_on %} {{ fmt_date(t.done_on) }}{% endif %}{% else %}open{% endif %} {{ todo_edit(company, t, t.index, t.contact.slug if t.contact else '') }} {{ todo_actions(company, t, delete=true) }} {% if is_next and t.due %}Add to Google Calendar{% endif %}
No tasks yet.
{% if type_names() %}{% endif %}
{%- endmacro %} {% macro task_list(company, tasks, today, contact='', adding=true) -%}
{% for t in tasks %} {% if type_names() %}{{ type_cell(company, t, loop.index0, contact) }}{% endif %} {% else %} {% endfor %}
{{ t.text }}{% if not type_names() %} {{ type_chip(t.type) }}{% endif %}{{ fmt_date(t.due) if t.due else '' }} {% if t.done %}done{% if t.done_on %} {{ fmt_date(t.done_on) }}{% endif %}{% endif %} {{ todo_edit(company, t, loop.index0, contact) }}
Nothing on the list.
{% if adding %}
{% if type_names() %}{% endif %}
{% endif %} {%- endmacro %} {# The handoff to the user's own agent (Make it yours): a request box and the buttons for the agent config.toml names. static/adjust.js builds the prompt, the link and the command from these data attributes as the user types, the same way adjust.handoff() does; the hint lines below say what each one does. #} {% macro handoff(h, id, page_title='', path='', label='Describe a change', text='', more=false) %}
{% if h.how == 'link' %}{{ h.button }}{% endif %} {% if h.how == 'command' %}{% endif %} {% if more %}More ideas{% endif %}
{% if h.how == 'link' %}{% endif %} {% if h.agent == 'claude' %}

Opens a terminal in your data folder with the request typed in. Nothing runs until you press Enter. Nothing opened? Run claude once in a terminal, then try again.

{% elif h.agent == 'cursor' %}

Open your data folder ({{ h.folder_shown }}) in Cursor first: the request goes to the Cursor window in front. Nothing runs until you press Enter.

{% elif h.how == 'command' %}

Paste the command in a terminal and press Enter: {{ h.label }} starts in your data folder with the request.

{% else %}

Paste the prompt into your AI agent, started in your data folder ({{ h.folder_shown }}). Name your agent to get a one-click button.

{% endif %}
What your agent receives

{% endmacro %} {#- Under a filter problem: opens Ask · Adjust on the Adjust tab with `request` filled in (adjust.js). Hidden until adjust.js has wired it, so it never shows as a button that does nothing. -#} {% macro fix_with_ai(request) -%} {%- endmacro %} {#- The one-click upgrade button (upgrade.js follows it; without JavaScript it posts to /upgrade, a plain page). `up` is upgrade_banner(): kind 'available' or 'restart'. -#} {% macro upgrade_form(up) -%}
{%- endmacro %} {# A closed overflow for the rare actions of a card or row: a plain
, so it works without a script. Use as {% call more() %}{% endcall %}. #} {% macro more(label='More') %}
{{ label }}
{{ caller() }}
{% endmacro %}