Lunaya

Making a theme

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.

Start from a file that already works

  1. Download any theme from the gallery — the .json source sits beside the .lunayatheme package.
  2. Change 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.
  3. Change name and the colours. Keep basedOn — it inherits every field you don't write.
  4. Rename the file to <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.

Colours

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
paperThe paper — background of the whole sheet
paperShadeA second ground close to the paper — selected cells, chips
inkMain ink — the date figure, body text
inkFaintFaint ink — small labels and captions
vermilionVermilion — holiday names and Sundays
sealSeal red — the ring around today
jadeJade — zodiac icon, sexagenary text
goldGilt — rare accent
dividerRules and separators
surfaceCard surface raised off the paper
onSurfaceText on a card surface

The contrast gate

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.

Paper texture

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

Out of range means clamped, not rejected

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:

Fieldminmax
typography.scale.dayNumber 0.71.4
typography.scale.body 0.71.4
texture.grainAlpha 00.25
texture.stainAlpha 00.25
texture.inkBleed 01
shapes.cornerRadius 024
shapes.borderWidth 04
shapes.dividerWidth 04
component.style.scale 0.71.4
component.style.opacity 0.21
texture.patternAlpha 00.12
texture.patternScale 0.52.5

Light and dark

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.

Rearranging the day sheet

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 minmax Components allowed
header 02 weekday month-year holiday-banner image divider spacer
hero 11 day-number divider spacer
lunar 02 weekday lunar-date can-chi holiday-banner divider spacer
info 06 lunar-date can-chi holiday-banner solar-term good-hours quote event-list image divider spacer
footer 02 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

Package layout and limits

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
LimitValue
fileBytes2097152
idMaxLength128
nameMaxLength60
authorMaxLength60
descriptionMaxLength200
maxImages2
jsonMaxDepth32
archiveBytes2097152
archiveUncompressedBytes8388608
archiveMaxEntries8
manifestBytes262144
archiveEntryNamePattern^(theme\.json|assets/[a-z0-9._-]{1,64})$
archiveMaxEntryBytes4194304

When something is wrong

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.

CodeMeaning
ERR_FORMAT_VERSION_UNSUPPORTEDformatVersion lớn hơn bản app hiểu
ERR_MISSING_REQUIREDThiếu trường bắt buộc
ERR_ID_INVALIDid sai định dạng hoặc quá dài
ERR_COLOR_FORMATMàu không phải #RRGGBB hoặc #AARRGGBB
ERR_TOO_LARGEFile vượt giới hạn dung lượng
ERR_BASEDON_UNKNOWNbasedOn trỏ tới theme dựng sẵn không tồn tại
ERR_SLOT_UNKNOWNTê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_SLOTLoại thành phần không được phép trong ô này
ERR_TOO_MANY_IMAGESVượt số ảnh cho phép
WARN_CONTRAST_LOWTương phản ink/paper dưới 4.5:1
WARN_CONTRAST_VERY_LOWTương phản ink/paper dưới 3.0:1 — rất khó đọc
WARN_COMPONENT_UNKNOWNLoại thành phần không biết — đã bỏ qua
WARN_FONT_SLUG_UNKNOWNSlug font không biết — dùng font mặc định
WARN_VALUE_CLAMPEDGiá trị vượt biên — đã kẹp lại
ERR_ARCHIVE_INVALIDGói nén hỏng hoặc không đọc được
ERR_ARCHIVE_ENTRY_NAMETên mục trong gói không nằm trong danh sách cho phép
ERR_ARCHIVE_TOO_MANY_ENTRIESGói có quá nhiều mục
ERR_ARCHIVE_UNCOMPRESSED_TOO_LARGETổng cỡ sau giải nén vượt trần
ERR_MANIFEST_MISSINGGói thiếu theme.json
ERR_ARCHIVE_ENTRY_TOO_LARGEMộ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_UNKNOWNThành phần image trỏ tới khoá không có trong images
ERR_IMAGE_SOURCE_INVALIDNguồn ảnh không phải assets/<tên> hay data: base64
WARN_IMAGE_SRC_MISSINGThà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.