A theme is one small JSON file: a palette, a paper texture, corner radii, and optionally a different arrangement of the blocks on the day sheet. Take one from the gallery, change what you want, and import it back.
.json source sits beside the
.lunayatheme package.id to something of your own. An id that matches a built-in
theme is silently ignored by the app: no error, no new theme. The nine taken
ids are do-tet · gach-bong · giay-cu · giay-do · giay-moi · muc-dem · nuoc-luc · son-mai · truc-xanh.name and the colours. Keep basedOn — it inherits
every field you don't write.<your-id>.lunayatheme and import it: in Lunaya,
Settings → Appearance → Import theme from file. Plain JSON works too;
the app checks the first four bytes and reads either form.Eleven tokens. Every colour in the app comes from one of them, so a theme never names a colour twice. Values are #RRGGBB.
| Token | Used for |
|---|---|
paper | The paper — background of the whole sheet |
paperShade | A second ground close to the paper — selected cells, chips |
ink | Main ink — the date figure, body text |
inkFaint | Faint ink — small labels and captions |
vermilion | Vermilion — holiday names and Sundays |
seal | Seal red — the ring around today |
jade | Jade — zodiac icon, sexagenary text |
gold | Gilt — rare accent |
divider | Rules and separators |
surface | Card surface raised off the paper |
onSurface | Text on a card surface |
The app measures the WCAG contrast ratio between ink and paper. Below 4.5 it warns; below 3 it warns harder. It never refuses the file — a theme you can barely read is still your choice, and the warning tells you which pair is at fault.
Two tokens are easy to forget: inkFaint carries the small labels, and vermilion carries holiday names and Sundays. Both are real text — check them against paper too, not just ink.
Grain, staining and ink bleed are drawn by the app over the paper colour. Patterns are drawn in code, not images, so they follow your ink colour and stay sharp at any density. Five slugs:
hoi-van · song-nuoc · tram-cham · mat-vai · hoa-thi
Write 5.0 where the range stops at 1.4 and the app uses 1.4 — it does not throw the file away. That is deliberate: a number nudged out of range should not cost you the whole theme. Ranges read from the spec:
| Field | min | max |
|---|---|---|
typography.scale.dayNumber |
0.7 | 1.4 |
typography.scale.body |
0.7 | 1.4 |
texture.grainAlpha |
0 | 0.25 |
texture.stainAlpha |
0 | 0.25 |
texture.inkBleed |
0 | 1 |
shapes.cornerRadius |
0 | 24 |
shapes.borderWidth |
0 | 4 |
shapes.dividerWidth |
0 | 4 |
component.style.scale |
0.7 | 1.4 |
component.style.opacity |
0.2 | 1 |
texture.patternAlpha |
0 | 0.12 |
texture.patternScale |
0.5 | 2.5 |
Set isDark to say which one your palette is. A light theme may also carry darkVariant — the same token names, different values — and the app swaps to it when the palette needs a dark counterpart. Omit it and the theme simply stays as written.
The Today screen is not a fixed layout — the app draws it from a tree your theme can describe. You place components into named slots; you do not set coordinates. Free coordinates break on every screen size other than yours, and a broken layout cannot be clamped back the way a colour can.
| Slot | min | max | Components allowed |
|---|---|---|---|
header |
0 | 2 | weekday month-year holiday-banner image divider spacer |
hero |
1 | 1 | day-number divider spacer |
lunar |
0 | 2 | weekday lunar-date can-chi holiday-banner divider spacer |
info |
0 | 6 | lunar-date can-chi holiday-banner solar-term good-hours quote event-list image divider spacer |
footer |
0 | 2 | month-year quote image divider spacer |
What a component does when tapped is decided by the app, by component type — a theme
cannot change it. Fonts: be-vietnam-pro · playfair-display · bitter
A .lunayatheme file is a zip with theme.json at the root and, if you have images, an assets/ folder beside it. Entry names must match the pattern below exactly — it is an allow-list, not a block-list.
ten-giao-dien.lunayatheme
├── theme.json
└── assets/
└── hoa-van.png
| Limit | Value |
|---|---|
fileBytes | 2097152 |
idMaxLength | 128 |
nameMaxLength | 60 |
authorMaxLength | 60 |
descriptionMaxLength | 200 |
maxImages | 2 |
jsonMaxDepth | 32 |
archiveBytes | 2097152 |
archiveUncompressedBytes | 8388608 |
archiveMaxEntries | 8 |
manifestBytes | 262144 |
archiveEntryNamePattern | ^(theme\.json|assets/[a-z0-9._-]{1,64})$ |
archiveMaxEntryBytes | 4194304 |
Errors have stable codes, so the app and the web editor can disagree in wording but never in meaning. ERR_ refuses the file; WARN_ accepts it and tells you.
| Code | Meaning |
|---|---|
ERR_FORMAT_VERSION_UNSUPPORTED | formatVersion lớn hơn bản app hiểu |
ERR_MISSING_REQUIRED | Thiếu trường bắt buộc |
ERR_ID_INVALID | id sai định dạng hoặc quá dài |
ERR_COLOR_FORMAT | Màu không phải #RRGGBB hoặc #AARRGGBB |
ERR_TOO_LARGE | File vượt giới hạn dung lượng |
ERR_BASEDON_UNKNOWN | basedOn trỏ tới theme dựng sẵn không tồn tại |
ERR_SLOT_UNKNOWN | Tên ô chứa không có trong slots.json |
ERR_SLOT_OVERFLOW | Ô chứa vượt số thành phần cho phép |
ERR_HERO_EMPTY | Ô hero bắt buộc có đúng 1 thành phần |
ERR_COMPONENT_NOT_ALLOWED_IN_SLOT | Loại thành phần không được phép trong ô này |
ERR_TOO_MANY_IMAGES | Vượt số ảnh cho phép |
WARN_CONTRAST_LOW | Tương phản ink/paper dưới 4.5:1 |
WARN_CONTRAST_VERY_LOW | Tương phản ink/paper dưới 3.0:1 — rất khó đọc |
WARN_COMPONENT_UNKNOWN | Loại thành phần không biết — đã bỏ qua |
WARN_FONT_SLUG_UNKNOWN | Slug font không biết — dùng font mặc định |
WARN_VALUE_CLAMPED | Giá trị vượt biên — đã kẹp lại |
ERR_ARCHIVE_INVALID | Gói nén hỏng hoặc không đọc được |
ERR_ARCHIVE_ENTRY_NAME | Tên mục trong gói không nằm trong danh sách cho phép |
ERR_ARCHIVE_TOO_MANY_ENTRIES | Gói có quá nhiều mục |
ERR_ARCHIVE_UNCOMPRESSED_TOO_LARGE | Tổng cỡ sau giải nén vượt trần |
ERR_MANIFEST_MISSING | Gói thiếu theme.json |
ERR_ARCHIVE_ENTRY_TOO_LARGE | Một mục giải nén vượt trần (chặn khi đọc, không tin cỡ khai trong zip) |
ERR_IMAGE_SRC_UNKNOWN | Thành phần image trỏ tới khoá không có trong images |
ERR_IMAGE_SOURCE_INVALID | Nguồn ảnh không phải assets/<tên> hay data: base64 |
WARN_IMAGE_SRC_MISSING | Thành phần image không khai src — sẽ không vẽ gì |
Every number on this page is read from the app's own spec when the page is built, so it cannot drift out of date.