Office: ready-made templates, the file-type badge no longer overlaps the date, and a Full editor and Edit elsewhere option for phones
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
16ba5de7c5
commit
da6c30fac4
16 files changed
+202
-11
No files matched your search
@@ -0,0 +1,168 @@
|
||||
# Makes the ready-made Office templates in public/templates. Needs: pip install python-docx openpyxl python-pptx. Run: python3 tools/make-office-templates.py public/templates
|
||||
import sys, os
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches as PI, Pt as PP
|
||||
from pptx.dml.color import RGBColor as PC
|
||||
|
||||
OUT = sys.argv[1]; os.makedirs(OUT, exist_ok=True)
|
||||
BLUE = '3D6BF0'
|
||||
|
||||
# ---------------- Word
|
||||
def base(doc, size=11):
|
||||
st = doc.styles['Normal']; st.font.name = 'Calibri'; st.font.size = Pt(size)
|
||||
for s in doc.sections: s.left_margin = s.right_margin = Inches(1); s.top_margin = s.bottom_margin = Inches(0.9)
|
||||
def para(doc, text='', bold=False, size=None, color=None, align=None, after=6, italic=False):
|
||||
p = doc.add_paragraph(); r = p.add_run(text); r.bold = bold; r.italic = italic
|
||||
if size: r.font.size = Pt(size)
|
||||
if color: r.font.color.rgb = RGBColor.from_string(color)
|
||||
if align: p.alignment = align
|
||||
p.paragraph_format.space_after = Pt(after); return p
|
||||
def heading(doc, text, size=13):
|
||||
p = para(doc, text.upper(), bold=True, size=size - 2, color=BLUE, after=3); p.paragraph_format.space_before = Pt(10)
|
||||
return p
|
||||
|
||||
d = Document(); base(d, 11)
|
||||
para(d, '[Your name]', bold=True, size=14, after=0); para(d, '[Street address]', after=0); para(d, '[City, State ZIP]', after=0); para(d, '[Phone] · [Email]', after=14)
|
||||
para(d, '[Date]', after=14); para(d, '[Recipient name]', after=0); para(d, '[Title or company]', after=0); para(d, '[Street address]', after=0); para(d, '[City, State ZIP]', after=14)
|
||||
para(d, 'Dear [Name],', after=10)
|
||||
para(d, 'Start with the reason you are writing, in a sentence or two. Keep it friendly and direct.', after=10)
|
||||
para(d, 'Use the middle paragraph for the details: what you need, what happened, or what you are offering. Short paragraphs are easier to read.', after=10)
|
||||
para(d, 'Close by saying what happens next, and thank them for their time.', after=14)
|
||||
para(d, 'Sincerely,', after=30); para(d, '[Your name]', after=0)
|
||||
d.save(f'{OUT}/letter.docx')
|
||||
|
||||
d = Document(); base(d, 10.5)
|
||||
para(d, '[Your Name]', bold=True, size=24, after=0); para(d, '[City, State] · [Phone] · [Email]', color='666666', after=8)
|
||||
heading(d, 'Summary'); para(d, 'Two or three sentences about who you are, what you do well, and what you are looking for.')
|
||||
heading(d, 'Experience')
|
||||
for i in range(2):
|
||||
para(d, '[Job title], [Employer]', bold=True, after=0); para(d, '[Month Year] – [Month Year]', italic=True, color='666666', after=3)
|
||||
for t in ['What you did and the result it had (use numbers when you can)', 'Something you improved, built or led', 'A skill you used every day']:
|
||||
p = d.add_paragraph(t, style='List Bullet'); p.paragraph_format.space_after = Pt(1)
|
||||
para(d, '', after=4)
|
||||
heading(d, 'Education'); para(d, '[School], [Degree or program]', bold=True, after=0); para(d, '[Year]', italic=True, color='666666')
|
||||
heading(d, 'Skills'); para(d, '[Skill] · [Skill] · [Skill] · [Skill] · [Skill]')
|
||||
d.save(f'{OUT}/resume.docx')
|
||||
|
||||
d = Document(); base(d, 11)
|
||||
para(d, 'Meeting notes', bold=True, size=22, color=BLUE, after=2); para(d, '[Meeting name]', size=13, after=10)
|
||||
t = d.add_table(rows=4, cols=2); t.style = 'Light Grid Accent 1'
|
||||
for i, (a, b) in enumerate([('Date', '[Date and time]'), ('Where', '[Place or link]'), ('Who was there', '[Names]'), ('Taking notes', '[Name]')]): t.cell(i, 0).text = a; t.cell(i, 1).text = b
|
||||
para(d, '', after=4); heading(d, 'Agenda')
|
||||
for x in ['[First topic]', '[Second topic]', '[Third topic]']: d.add_paragraph(x, style='List Number')
|
||||
heading(d, 'Notes'); para(d, 'What was said and what was decided.', italic=True, color='666666'); para(d, '', after=30)
|
||||
heading(d, 'Action items')
|
||||
t = d.add_table(rows=4, cols=3); t.style = 'Light Grid Accent 1'
|
||||
for i, h in enumerate(['What needs doing', 'Who', 'By when']): t.cell(0, i).text = h
|
||||
d.save(f'{OUT}/meeting-notes.docx')
|
||||
|
||||
d = Document(); base(d, 11)
|
||||
para(d, '[Family Name] News', bold=True, size=28, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, after=0); para(d, '[Month Year]', align=WD_ALIGN_PARAGRAPH.CENTER, color='666666', after=14)
|
||||
heading(d, 'The big news'); para(d, 'Share the headline: a trip, a birthday, a new job, a new pet. A few friendly sentences is plenty.')
|
||||
heading(d, 'What everyone has been up to')
|
||||
for n in ['[Name]', '[Name]', '[Name]']: para(d, n, bold=True, after=0); para(d, 'A line or two about school, work, hobbies or something funny.', after=8)
|
||||
heading(d, 'Coming up'); t = d.add_table(rows=4, cols=2); t.style = 'Light List Accent 1'
|
||||
t.cell(0, 0).text = 'Date'; t.cell(0, 1).text = 'What'
|
||||
heading(d, 'A favorite moment'); para(d, 'Add a photo here (Insert → Image) and a sentence about it.', italic=True, color='666666')
|
||||
d.save(f'{OUT}/family-newsletter.docx')
|
||||
|
||||
d = Document(); base(d, 11)
|
||||
para(d, '[Recipe name]', bold=True, size=24, color=BLUE, after=2); para(d, 'Serves [4] · Prep [15 min] · Cook [30 min]', color='666666', after=10)
|
||||
heading(d, 'Ingredients')
|
||||
for x in ['[2 cups flour]', '[1 tsp salt]', '[1 cup milk]', '[2 eggs]']: d.add_paragraph(x, style='List Bullet')
|
||||
heading(d, 'Steps')
|
||||
for x in ['[First step]', '[Second step]', '[Third step]']: d.add_paragraph(x, style='List Number')
|
||||
heading(d, 'Notes'); para(d, 'Where it came from, what to change next time, what goes well with it.', italic=True, color='666666')
|
||||
d.save(f'{OUT}/recipe-card.docx')
|
||||
|
||||
# ---------------- Excel
|
||||
thin = Side(style='thin', color='D0D5DD'); BOX = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
HEAD = PatternFill('solid', fgColor=BLUE); SOFT = PatternFill('solid', fgColor='EEF2FF')
|
||||
def hdr(ws, row, values, widths=None):
|
||||
for i, v in enumerate(values, 1):
|
||||
c = ws.cell(row=row, column=i, value=v); c.font = Font(bold=True, color='FFFFFF'); c.fill = HEAD; c.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True); c.border = BOX
|
||||
if widths:
|
||||
for i, w in enumerate(widths, 1): ws.column_dimensions[get_column_letter(i)].width = w
|
||||
def title(ws, text, sub=None):
|
||||
ws['A1'] = text; ws['A1'].font = Font(bold=True, size=18, color=BLUE)
|
||||
if sub: ws['A2'] = sub; ws['A2'].font = Font(italic=True, color='666666')
|
||||
|
||||
wb = Workbook(); ws = wb.active; ws.title = 'Budget'; title(ws, 'Monthly budget', 'Fill in the planned and actual amounts. The last column does the math.')
|
||||
ws['A4'] = 'Income'; ws['A4'].font = Font(bold=True, size=12)
|
||||
hdr(ws, 5, ['Item', 'Planned', 'Actual', 'Difference'], [30, 14, 14, 14])
|
||||
inc = ['Paycheck', 'Second income', 'Other']
|
||||
for i, n in enumerate(inc):
|
||||
r = 6 + i; ws.cell(r, 1, n); ws.cell(r, 2, 0); ws.cell(r, 3, 0); ws.cell(r, 4, f'=C{r}-B{r}')
|
||||
r = 6 + len(inc); ws.cell(r, 1, 'Total income').font = Font(bold=True); ws.cell(r, 2, f'=SUM(B6:B{r-1})'); ws.cell(r, 3, f'=SUM(C6:C{r-1})'); ws.cell(r, 4, f'=C{r}-B{r}'); inc_total = r
|
||||
for c in range(1, 5): ws.cell(r, c).fill = SOFT; ws.cell(r, c).font = Font(bold=True)
|
||||
start = r + 2; ws.cell(start, 1, 'Expenses').font = Font(bold=True, size=12); hdr(ws, start + 1, ['Item', 'Planned', 'Actual', 'Difference'])
|
||||
exp = ['Rent or mortgage', 'Utilities', 'Groceries', 'Transportation', 'Insurance', 'Phone and internet', 'Eating out', 'Kids and school', 'Health', 'Giving', 'Savings', 'Fun', 'Other']
|
||||
first = start + 2
|
||||
for i, n in enumerate(exp):
|
||||
r = first + i; ws.cell(r, 1, n); ws.cell(r, 2, 0); ws.cell(r, 3, 0); ws.cell(r, 4, f'=B{r}-C{r}')
|
||||
last = first + len(exp) - 1; r = last + 1; ws.cell(r, 1, 'Total expenses'); ws.cell(r, 2, f'=SUM(B{first}:B{last})'); ws.cell(r, 3, f'=SUM(C{first}:C{last})'); ws.cell(r, 4, f'=B{r}-C{r}'); exp_total = r
|
||||
for c in range(1, 5): ws.cell(r, c).fill = SOFT; ws.cell(r, c).font = Font(bold=True)
|
||||
r += 2; ws.cell(r, 1, 'Left over (income − expenses)').font = Font(bold=True, size=12); ws.cell(r, 2, f'=B{inc_total}-B{exp_total}'); ws.cell(r, 3, f'=C{inc_total}-C{exp_total}')
|
||||
for row in ws.iter_rows(min_row=6, max_row=r):
|
||||
for c in row[1:4]: c.number_format = '$#,##0.00;[Red]-$#,##0.00'
|
||||
for c in row[:4]:
|
||||
if c.row not in (start, start + 1, r) and c.value is not None: c.border = BOX
|
||||
ws.freeze_panes = 'A6'; ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/monthly-budget.xlsx')
|
||||
|
||||
wb = Workbook(); ws = wb.active; ws.title = 'Chores'; title(ws, 'Weekly chore chart', 'Put a name in each box, or a check mark when it is done.')
|
||||
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; hdr(ws, 4, ['Chore'] + days + ['Points'], [26] + [10] * 7 + [10])
|
||||
chores = ['Make bed', 'Set the table', 'Clear the table', 'Dishes / dishwasher', 'Feed the pets', 'Take out trash', 'Tidy bedroom', 'Laundry', 'Vacuum', 'Water plants']
|
||||
for i, n in enumerate(chores):
|
||||
r = 5 + i; ws.cell(r, 1, n)
|
||||
for c in range(1, 10): ws.cell(r, c).border = BOX; ws.cell(r, c).alignment = Alignment(horizontal='center' if c > 1 else 'left')
|
||||
ws.cell(r, 9, f'=COUNTA(B{r}:H{r})')
|
||||
r = 5 + len(chores); ws.cell(r, 1, 'Total').font = Font(bold=True); ws.cell(r, 9, f'=SUM(I5:I{r-1})'); ws.cell(r, 9).font = Font(bold=True)
|
||||
for c in range(1, 10): ws.cell(r, c).fill = SOFT; ws.cell(r, c).border = BOX
|
||||
ws.freeze_panes = 'B5'; ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/chore-chart.xlsx')
|
||||
|
||||
wb = Workbook(); ws = wb.active; ws.title = 'Meals'; title(ws, 'Weekly meal planner', 'Plan the week, then build the shopping list from it.')
|
||||
hdr(ws, 4, ['', 'Breakfast', 'Lunch', 'Dinner', 'Snacks'], [14, 24, 24, 28, 22])
|
||||
for i, d_ in enumerate(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']):
|
||||
r = 5 + i; ws.cell(r, 1, d_).font = Font(bold=True); ws.cell(r, 1).fill = SOFT
|
||||
for c in range(1, 6): ws.cell(r, c).border = BOX; ws.cell(r, c).alignment = Alignment(vertical='top', wrap_text=True)
|
||||
ws.row_dimensions[r].height = 34
|
||||
ws.cell(13, 1, 'Shopping list').font = Font(bold=True, size=12); ws.cell(14, 1, 'Write what you need to buy under here.').font = Font(italic=True, color='666666')
|
||||
ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/meal-planner.xlsx')
|
||||
|
||||
wb = Workbook(); ws = wb.active; ws.title = 'Expenses'; title(ws, 'Expense tracker', 'One line per purchase. The totals below add themselves up.')
|
||||
hdr(ws, 4, ['Date', 'What', 'Category', 'Paid with', 'Amount'], [14, 32, 18, 16, 14])
|
||||
for r in range(5, 45):
|
||||
for c in range(1, 6): ws.cell(r, c).border = BOX
|
||||
ws.cell(r, 1).number_format = 'mmm d, yyyy'; ws.cell(r, 5).number_format = '$#,##0.00'
|
||||
ws['D46'] = 'Total'; ws['D46'].font = Font(bold=True); ws['E46'] = '=SUM(E5:E44)'; ws['E46'].number_format = '$#,##0.00'; ws['E46'].font = Font(bold=True)
|
||||
ws['G4'] = 'By category'; ws['G4'].font = Font(bold=True, color='FFFFFF'); ws['G4'].fill = HEAD; ws['H4'] = 'Total'; ws['H4'].font = Font(bold=True, color='FFFFFF'); ws['H4'].fill = HEAD; ws.column_dimensions['G'].width = 18; ws.column_dimensions['H'].width = 14
|
||||
for i, n in enumerate(['Groceries', 'Gas', 'Eating out', 'Home', 'Kids', 'Health', 'Fun', 'Other']):
|
||||
r = 5 + i; ws.cell(r, 7, n); ws.cell(r, 8, f'=SUMIF($C$5:$C$44,G{r},$E$5:$E$44)'); ws.cell(r, 8).number_format = '$#,##0.00'
|
||||
ws.freeze_panes = 'A5'; ws.sheet_view.showGridLines = False; wb.save(f'{OUT}/expense-tracker.xlsx')
|
||||
|
||||
# ---------------- PowerPoint
|
||||
prs = Presentation(); prs.slide_width = PI(13.333); prs.slide_height = PI(7.5)
|
||||
def slide(title_text, lines=None, layout=1, sub=None):
|
||||
s = prs.slides.add_slide(prs.slide_layouts[layout]); s.shapes.title.text = title_text
|
||||
for p in s.shapes.title.text_frame.paragraphs:
|
||||
for r in p.runs: r.font.color.rgb = PC.from_string(BLUE); r.font.bold = True; r.font.name = 'Calibri'
|
||||
if layout == 0 and sub is not None: s.placeholders[1].text = sub
|
||||
elif lines is not None:
|
||||
tf = s.placeholders[1].text_frame; tf.clear()
|
||||
for i, t in enumerate(lines):
|
||||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph(); p.text = t; p.font.size = PP(26); p.font.name = 'Calibri'
|
||||
return s
|
||||
slide('[Presentation title]', layout=0, sub='[Your name] · [Date]')
|
||||
slide('Agenda', ['[First topic]', '[Second topic]', '[Third topic]', 'Questions'])
|
||||
slide('[A key point]', ['Say it in one clear sentence', 'Back it up with two or three short points', 'Add a picture or a chart on the right (Insert → Image)'])
|
||||
slide('[Another key point]', ['Keep to a few words per line', 'Let the pictures do the talking', 'Practice saying the rest out loud'])
|
||||
slide('Summary', ['What we covered', 'What happens next', 'Who does what'])
|
||||
slide('Thank you', layout=0, sub='[Contact or question prompt]')
|
||||
prs.save(f'{OUT}/simple-presentation.pptx')
|
||||
print(sorted(os.listdir(OUT)))
|
||||
Reference in new issue
Block a user