diff --git a/docs/nostr-static-client-spec.md b/docs/nostr-static-client-spec.md index 3adb360..c70da3d 100644 --- a/docs/nostr-static-client-spec.md +++ b/docs/nostr-static-client-spec.md @@ -100,6 +100,8 @@ Default behavior: `qr_target: "flyer_url"`. When publishing, the client computes the replaceable-event `naddr` and writes the generated poster URL to `tear_off_link`. The tear-off tabs and tear-off QR code point at `tear_off_link`, so people can re-open and reprint the flyer. The main flyer QR code points at `url`, so viewers can visit the creator's intended destination. +The `naddr` is encoded with a **single relay hint** (the primary configured relay) instead of the full relay list. This keeps the poster URL short and its tear-off QR code easy to scan — a typical poster URL drops from ~233 characters (full list) to ~171 — while still pointing a fresh viewer at a relay that has the event. Viewers also fall back to `DEFAULT_RELAYS`, which is where flyers are published by default. This stays a standard NIP-19 `naddr`, so any Nostr client can resolve it, and older relay-bearing `naddr` URLs continue to decode and resolve unchanged. + Supported values: - `flyer_url`: tear-off QR points to the hosted client URL for the poster @@ -135,6 +137,27 @@ Language changes should: - update printed side-tab labels - respect RTL direction metadata +### Letter-spacing by script + +Flyer text uses tighter letter-spacing than the original design to fit more +characters per line, but `letter-spacing` is not script-neutral, so it is +applied per script via a policy class on the flyer `.container` +(`LETTER_SPACING_POLICY` in `nostr-client.js`; `ls-full` / `ls-cjk` / `ls-none` +rules and `--ls-*` variables in `nostr-client.css`): + +- `full` — Latin, Cyrillic, Hebrew: reduced positive tracking on the display + fields plus a slight negative on body/URL to reclaim line width. +- `cjk` — Japanese, Chinese: tracking is clamped to `0` and never negative, so + full-width glyphs do not overlap. +- `none` — Arabic, Persian, Hindi: letter-spacing is turned off entirely. These + are cursive or complex scripts where any tracking breaks the letter joins or + detaches combining marks. (This also corrected the prior behavior, which + applied the design's display tracking to every language unconditionally.) + +When adding a language, classify its script in `LETTER_SPACING_POLICY` (default +is `full`) and verify the headline, subtitle, and body render correctly in the +browser. + ## Verification ```bash diff --git a/site/nostr/index.html b/site/nostr/index.html index 86e896d..24108b3 100644 --- a/site/nostr/index.html +++ b/site/nostr/index.html @@ -104,8 +104,6 @@ Footer message -

-
diff --git a/site/nostr/nostr-client.css b/site/nostr/nostr-client.css index f009078..6696623 100644 --- a/site/nostr/nostr-client.css +++ b/site/nostr/nostr-client.css @@ -258,8 +258,8 @@ code { color: var(--muted); } -.field-fit-status { - margin: 0 0 10px; +.field-error { + margin: 0; color: var(--warn); font-size: 12px; font-weight: 700; @@ -382,6 +382,41 @@ a { box-sizing: border-box; overflow: hidden; page-break-inside: avoid; + /* Per-script letter-spacing, set by the ls-* policy class (see + LETTER_SPACING_POLICY in nostr-client.js). Defaults below match the + historical design and act as a safe fallback if no policy class is set. */ + --ls-display: 2px; + --ls-subtitle: 6px; + --ls-body: 0; + --ls-url: 0; + --ls-footer: 0; +} + +/* Latin, Cyrillic, Hebrew: tighten tracking to reclaim line space. */ +.container.ls-full { + --ls-display: 0.5px; + --ls-subtitle: 2px; + --ls-body: -0.3px; + --ls-url: -0.3px; + --ls-footer: 0; +} + +/* CJK: tracking is safe but must never go negative (glyphs would overlap). */ +.container.ls-cjk { + --ls-display: 0; + --ls-subtitle: 0; + --ls-body: 0; + --ls-url: 0; + --ls-footer: 0; +} + +/* Arabic, Persian, Hindi: letter-spacing breaks shaping — turn it off. */ +.container.ls-none { + --ls-display: normal; + --ls-subtitle: normal; + --ls-body: normal; + --ls-url: normal; + --ls-footer: normal; } .container.no-tear-offs .left-tear-offs { @@ -410,6 +445,26 @@ a { overflow-wrap: anywhere; word-break: normal; hyphens: none; + display: flex; + flex-direction: column; +} + +.tear-off-event-id { + margin-top: auto; + padding-top: 4px; + font-size: 8px; + line-height: 1.2; + word-break: break-all; + overflow-wrap: anywhere; +} + +.tear-off-event-id-value { + display: block; + font-family: "Courier New", monospace; + font-size: 12px; + line-height: 1.1; + letter-spacing: -0.2px; + word-break: break-all; } .qr-code { @@ -463,7 +518,7 @@ a { padding: 0; font-size: 24px; line-height: 1.2; - letter-spacing: 2px; + letter-spacing: var(--ls-display); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -473,7 +528,7 @@ a { .distribute { font-size: 18px; line-height: 1.2; - letter-spacing: 6px; + letter-spacing: var(--ls-subtitle); margin: 2px 0; padding: 0; white-space: nowrap; @@ -487,6 +542,7 @@ a { margin-top: 4px; font-size: 14px; line-height: 1.4; + letter-spacing: var(--ls-body); text-align: left; overflow: hidden; display: flex; @@ -516,6 +572,7 @@ a { .qr-code-url { font-size: 12px; + letter-spacing: var(--ls-url); text-align: left; overflow-wrap: anywhere; word-break: normal; @@ -551,6 +608,7 @@ hr { .container .footer .binary { margin-bottom: 8px; margin-top: 10px; + letter-spacing: var(--ls-footer); } .container .footer .credit { diff --git a/site/nostr/nostr-client.js b/site/nostr/nostr-client.js index f2cc589..bcf8194 100644 --- a/site/nostr/nostr-client.js +++ b/site/nostr/nostr-client.js @@ -12,6 +12,23 @@ const FALLBACK_LANG = "en"; const LANGUAGE_SELECT_IDS = ["topbar-lang", "field-lang", "viewer-lang"]; const EVENT_KIND = 30078; + // Per-script letter-spacing policy for flyer text. `letter-spacing` is not + // script-neutral, so the flyer tightens tracking only where it is safe: + // "full" (Latin, Cyrillic, Hebrew): reduced positive tracking + slight + // negative on body/url to reclaim space per line. + // "cjk" (Japanese, Chinese): tracking clamped to 0 — full-width glyphs + // must not overlap, so never negative. + // "none" (Arabic, Persian, Hindi): letter-spacing OFF. These are cursive + // or complex scripts where any tracking breaks the letter joins or + // detaches combining marks; neutralizing it also fixes rendering. + // The values themselves live in nostr-client.css, keyed by the ls-* class. + const LETTER_SPACING_POLICY = { + ar: "none", fa: "none", hi: "none", + ja: "cjk", zh: "cjk" + }; + function trackingPolicy(lang) { + return LETTER_SPACING_POLICY[supportedLang(lang)] || "full"; + } const FIELD_LIMITS = { folder_name: 64, name: 120, @@ -119,15 +136,6 @@ Join us in a revolution that values truth and transparency. Together, we can bui "field-url": ".qr-code-url a", "field-footer-message": ".footer .binary" }; - const DISPLAY_FIT_LABEL_KEYS = { - "field-title": "labels.title", - "field-subtitle": "labels.subtitle", - "field-headline": "labels.headline", - "field-content": "labels.content", - "field-url-message": "labels.url_message", - "field-url": "labels.poster_url", - "field-footer-message": "labels.footer_message" - }; const NOSTR_UI = { ar: { back: "رجوع", @@ -141,7 +149,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "اللغة", load_event: "تحميل الحدث", normalized_config: "الإعداد الموحد", - open_poster: "افتح هذا الملصق:", + open_poster: "انقر لفتح هذا الملصق.", page_title: "عنوان الصفحة", preview: "معاينة", print: "اطبع المنشور", @@ -193,7 +201,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Sprache", load_event: "Event laden", normalized_config: "Normalisierte Konfiguration", - open_poster: "Dieses Plakat öffnen:", + open_poster: "Klicken Sie, um dieses Plakat zu öffnen.", page_title: "Seitentitel", preview: "Vorschau", print: "Flyer drucken", @@ -245,7 +253,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Language", load_event: "Load Event", normalized_config: "Normalized Config", - open_poster: "Open this poster:", + open_poster: "Click to open this poster.", page_title: "Page title", preview: "Preview", print: "Print this page", @@ -298,7 +306,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Idioma", load_event: "Cargar evento", normalized_config: "Configuración normalizada", - open_poster: "Abra este cartel:", + open_poster: "Haga clic para abrir este cartel.", page_title: "Título de la página", preview: "Vista previa", print: "Imprimir volante", @@ -350,7 +358,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "زبان", load_event: "بارگذاری رویداد", normalized_config: "پیکربندی عادی‌شده", - open_poster: "این پوستر را باز کنید:", + open_poster: "برای باز کردن این پوستر کلیک کنید.", page_title: "عنوان صفحه", preview: "پیش‌نمایش", print: "چاپ اعلامیه", @@ -402,7 +410,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Langue", load_event: "Charger l'evenement", normalized_config: "Configuration normalisee", - open_poster: "Ouvrez cette affiche :", + open_poster: "Cliquez pour ouvrir cette affiche.", page_title: "Titre de page", preview: "Apercu", print: "Imprimer le flyer", @@ -454,7 +462,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "שפה", load_event: "טען אירוע", normalized_config: "תצורה מנורמלת", - open_poster: "פתח את הכרזה הזו:", + open_poster: "לחץ כדי לפתוח כרזה זו.", page_title: "כותרת עמוד", preview: "תצוגה מקדימה", print: "הדפס פלייר", @@ -506,7 +514,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "भाषा", load_event: "इवेंट लोड करें", normalized_config: "सामान्यीकृत कॉन्फिग", - open_poster: "यह पोस्टर खोलें:", + open_poster: "इस पोस्टर को खोलने के लिए क्लिक करें।", page_title: "पृष्ठ शीर्षक", preview: "पूर्वावलोकन", print: "फ्लायर प्रिंट करें", @@ -558,7 +566,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "言語", load_event: "イベントを読み込む", normalized_config: "正規化設定", - open_poster: "このポスターを開く:", + open_poster: "クリックしてこのポスターを開く。", page_title: "ページタイトル", preview: "プレビュー", print: "フライヤーを印刷", @@ -610,7 +618,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Idioma", load_event: "Carregar evento", normalized_config: "Configuracao normalizada", - open_poster: "Abra este cartaz:", + open_poster: "Clique para abrir este cartaz.", page_title: "Titulo da pagina", preview: "Previa", print: "Imprimir Panfleto", @@ -662,7 +670,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Язык", load_event: "Загрузить событие", normalized_config: "Нормализованная конфигурация", - open_poster: "Откройте этот плакат:", + open_poster: "Нажмите, чтобы открыть этот плакат.", page_title: "Заголовок страницы", preview: "Предпросмотр", print: "Печать листовки", @@ -714,7 +722,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Lugha", load_event: "Pakia tukio", normalized_config: "Usanidi uliorekebishwa", - open_poster: "Fungua bango hili:", + open_poster: "Bofya kufungua bango hili.", page_title: "Kichwa cha ukurasa", preview: "Hakiki", print: "Chapa Kipeperushi", @@ -766,7 +774,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "Dil", load_event: "Olay yükle", normalized_config: "Normalleştirilmiş yapılandırma", - open_poster: "Bu posteri açın:", + open_poster: "Bu posteri açmak için tıklayın.", page_title: "Sayfa başlığı", preview: "Önizleme", print: "İlanı Yazdır", @@ -818,7 +826,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui language: "语言", load_event: "加载事件", normalized_config: "标准化配置", - open_poster: "打开此海报:", + open_poster: "点击打开此海报。", page_title: "页面标题", preview: "预览", print: "打印传单", @@ -861,6 +869,7 @@ Join us in a revolution that values truth and transparency. Together, we can bui }; let flyerSource = "default"; let loadedEventConfig = null; + let loadedEventId = ""; const lastAcceptedFieldValues = {}; const el = (id) => document.getElementById(id); @@ -918,18 +927,46 @@ Join us in a revolution that values truth and transparency. Together, we can bui return Boolean(node && node.dataset.emptyKey); } - function fieldLabel(fieldId, lang = currentUiLang()) { - const localePath = DISPLAY_FIT_LABEL_KEYS[fieldId]; - if (localePath) return translate(localePath, lang) || fieldId; + // The "too long" warning is shown inline under the offending field (so the + // issue is visible without scrolling to a single shared status line). The + // message element is created lazily inside the field's