Szablony paczek
Cztery paczki do pobrania i przerobienia na własne. Każda przechodzi nkb lint bez błędu i bez ostrzeżenia, a każda pokazuje inną część formatu.
Jak użyć szablonu?
Pobierz go do własnego folderu, a paleta od razu go pokaże. W palecie naciśnij AltShiftSpace
, kliknij Pack folders, potem Create and open, żeby otworzyć ten folder. Potem przerób szablon na własną paczkę: nadaj mu własne id, zmień tak samo nazwę pliku i zastąp wartości swoimi. Własna paczka objaśnia każdy wiersz.
Każdy szablon działa od razu, więc można wypróbować go w palecie, zanim cokolwiek się w nim zmieni. Szablony są po angielsku, a własną paczkę można pisać po polsku.
Pierwsza paczka
Trzy wartości dla pola z nazwiskiem: spacja na końcu, apostrof i pusta wartość. Najkrótsza paczka na start, z komentarzem przy wierszach, które są ważne za pierwszym razem.
# Your first pack: three values for a name field. Replace them with your own.
#
# A pack is this one file. Put it in your own folder: in the palette, open
# the value window, click Pack folders, then Create and open. Check it with
# `nkb lint first-pack.toml`, which names every problem and how to fix it.
#
# The guide that explains every line: https://naughtykeyboard.donislawdev.com/docs/write-a-pack/
#
# A template from https://naughtykeyboard.donislawdev.com/docs/pack-templates/
# Change anything in it. When you change `id`, rename the file to match,
# first-pack.toml now, because a pack is found by its file name.
format = 1
[pack]
id = "first-pack"
name = "First pack"
description = "Three values for a name field, to show how a pack is written."
version = "1.0"
updated = 2026-11-10
license = "CC-BY-4.0"
authors = ["Naughty Keyboard"]
language = "en"
tags = ["example", "names"]
fields = ["name", "text"]
risk = "normal"
# Each value is one [[values]] block. The palette types them in this order,
# so put the value most likely to find a bug first. A space at the end is
# written as \u0020, because an editor can drop one written out.
[[values]]
id = "trailing-space"
name = "Name with a space at the end"
value = "Kowalski\u0020"
breaks = "The sign-up form keeps the space and the login form trims it, or the other way round, so the account is created and then cannot be found."
expect = "Trimmed everywhere or kept everywhere, so the same name always finds the same account."
since = "1.0"
[[values]]
id = "apostrophe"
name = "Name with an apostrophe"
value = "O'Brien"
breaks = "Code that builds a query or a script by pasting text into it reads the apostrophe as the end of a string, so the save fails with a raw error or the name is cut short."
expect = "Saved and shown as O'Brien, with no error."
since = "1.0"
[[values]]
id = "empty"
name = "Empty value"
value = ""
breaks = "The browser treats an empty field as missing and the server as an empty string, or the other way round, so a required field is saved empty."
expect = "One clear answer on both sides: refused with a message, or saved as empty on purpose."
since = "1.0"
Pole z limitem długości
Wartości na granicy limitu 50 znaków: jeden znak poniżej, dokładnie limit i jeden znak ponad, a do tego ta sama długość w polskich literach, w emoji, w spacjach i w literach z osobnym akcentem, które inaczej liczą się w bajtach, w punktach kodowych i w jednostkach, które liczy przeglądarka. Zmień 50 na limit swojego pola, w każdym count i w każdej nazwie. Pokazuje, jak type = "repeat" zapisuje długą wartość w kilku wierszach.
# A field with a limit of 50 characters. Change 50 to the limit of your field
# in every `count` and every name below, and the pack tests that limit.
#
# A value this long is written as a short piece and a count, with
# type = "repeat", so the file stays readable.
#
# A template from https://naughtykeyboard.donislawdev.com/docs/pack-templates/
# Change anything in it. When you change `id`, rename the file to match,
# text-field-limit.toml now, because a pack is found by its file name.
format = 1
[pack]
id = "text-field-limit"
name = "Text field limit"
description = "Values at the edge of a 50-character limit, counted in letters, in bytes and in the units a browser counts."
version = "1.0"
updated = 2026-11-10
license = "CC-BY-4.0"
authors = ["Naughty Keyboard"]
language = "en"
tags = ["length", "limit", "boundary"]
fields = ["text", "name"]
risk = "normal"
[[values]]
id = "len-51"
name = "51 characters, one over the limit"
type = "repeat"
unit = "a"
count = 51
breaks = "A limit checked only in the browser lets it through when the request is sent another way, and a database column of 50 cuts the last letter or refuses the row with a raw error."
expect = "Refused with a message that names the limit, and never saved cut short."
since = "1.0"
[[values]]
id = "len-50"
name = "50 characters, exactly the limit"
type = "repeat"
unit = "a"
count = 50
breaks = "A check written as less than instead of at most refuses a value the field promises to take, so the longest allowed text is the one that fails."
expect = "Accepted and saved whole."
since = "1.0"
[[values]]
id = "polish-letters-50"
name = "50 Polish letters, 100 bytes"
type = "repeat"
unit = "ą"
count = 50
breaks = "Each of these letters takes two bytes in UTF-8, so a column or an API with a limit in bytes refuses or cuts a value the form counted as 50 characters."
expect = "Accepted whole, or refused with the same limit the form shows."
since = "1.0"
[[values]]
id = "emoji-50"
name = "50 emoji"
type = "repeat"
unit = "😀"
count = 50
breaks = "JavaScript counts length in UTF-16 units, two for each of these, so the form sees 100 where the server may see 50, and a cut in the middle of one leaves a broken character."
expect = "The browser and the server count the same way, and nothing is cut in half."
since = "1.0"
[[values]]
id = "spaces-51"
name = "51 spaces"
type = "repeat"
unit = "\u0020"
count = 51
breaks = "Over the limit before trimming and empty after it, so the answer depends on which check runs first, and a required field can be saved empty."
expect = "Refused as empty, with the same answer whichever check runs first."
since = "1.0"
[[values]]
id = "combining-50"
name = "50 letters with a separate accent"
type = "repeat"
unit = "e\u0301"
count = 50
breaks = "Each letter is two code points, an e and an accent, so a counter in code points sees 100 where a reader sees 50 letters, and a cut can drop the accent from the last one."
expect = "Counted as 50 letters, or normalised on save and counted the same way everywhere."
since = "1.0"
[[values]]
id = "len-49"
name = "49 characters, one under the limit"
type = "repeat"
unit = "a"
count = 49
breaks = "The control just under the limit. If it is refused too, the limit is smaller than the form says, and every other result here needs reading again."
expect = "Accepted and saved whole."
since = "1.0"
Formularz rejestracji
Pole na adres e-mail z drugim polem, które go powtarza, i login, który ma być unikalny. Pokazuje dwie pary: ten sam adres ze spacją na końcu i bez niej oraz dwa loginy, które wyglądają tak samo, a są zapisane w dwóch alfabetach, drugi z cyrylicką literą zapisaną jako sekwencja ucieczki.
# A sign-up form: an e-mail field with a second field that repeats it, and a
# login that has to be unique. Two pairs join values that are typed one after
# the other: the first half into one field, the second half into the next.
#
# A template from https://naughtykeyboard.donislawdev.com/docs/pack-templates/
# Change anything in it. When you change `id`, rename the file to match,
# sign-up-form.toml now, because a pack is found by its file name.
format = 1
[pack]
id = "sign-up-form"
name = "Sign-up form"
description = "Values for an e-mail field, the field that repeats it, and a login that has to be unique."
version = "1.0"
updated = 2026-11-10
license = "CC-BY-4.0"
authors = ["Naughty Keyboard"]
language = "en"
tags = ["sign-up", "email", "login"]
fields = ["email", "username"]
risk = "normal"
[[values]]
id = "email"
name = "E-mail address"
value = "jan.kowalski@example.com"
breaks = "The first half of a pair, and on its own a plain address every form takes. Type it into the e-mail field, then the next value into the field that repeats it."
expect = "Accepted."
since = "1.0"
[[values]]
id = "email-trailing-space"
name = "The same address with a space at the end"
value = "jan.kowalski@example.com\u0020"
breaks = "The second half of the pair. A form that trims one field and not the other says the two addresses differ, or saves the address with the space and no mail arrives."
expect = "Trimmed in both fields, and accepted as the same address."
since = "1.0"
[[values]]
id = "login-latin"
name = "Login in Latin letters"
value = "kowalski"
breaks = "The first half of a pair, and on its own an ordinary login. Sign up with it, then try to sign up again with the next value."
expect = "Accepted."
since = "1.0"
[[values]]
id = "login-cyrillic-o"
name = "The same login with a Cyrillic o"
value = "k\u043Ewalski"
breaks = "The second half. It looks exactly like the first login, but one letter is Cyrillic, so a uniqueness check that compares bytes makes a second account nobody can tell from the first."
expect = "Refused as taken, or accepted and shown so the difference can be seen."
since = "1.0"
[[values]]
id = "email-plus-tag"
name = "Address with a plus sign"
value = "jan.kowalski+shop@example.com"
breaks = "A check that allows only letters, digits and dots refuses a valid address many people use to sort their mail, and they cannot sign up at all."
expect = "Accepted, and the confirmation mail arrives."
since = "1.0"
[[pairs]]
id = "repeat-email"
name = "E-mail and the field that repeats it"
relation = "identity"
a = "email"
b = "email-trailing-space"
breaks = "The two fields are compared before they are trimmed, so a person who typed the same address twice is told the addresses differ."
expect = "Both fields trimmed the same way, and the two accepted as one address."
since = "1.0"
[[pairs]]
id = "look-alike-login"
name = "Two logins that look the same"
relation = "look-alike"
a = "login-latin"
b = "login-cyrillic-o"
breaks = "Two accounts with logins nobody can tell apart, so a message, a payment or an invitation goes to the wrong person."
expect = "The second login refused as taken, or both kept and shown so they can be told apart."
since = "1.0"
Daty rezerwacji
Zameldowanie o godzinie, którą zmiana czasu pomija albo powtarza, pobyt, który kończy się następnego ranka, i data daleko w przyszłości. Pokazuje type = "next-date", datę zapisaną jako reguła, więc nigdy nie przechodzi w przeszłość, i parę z początkiem i końcem.
# Booking dates: a check-in at an hour the clocks skip or repeat, a stay that
# ends the next morning, and a date far ahead. The two clock-change dates are
# written as a rule, type = "next-date", and worked out on the day they are
# typed, so they never fall into the past.
#
# A template from https://naughtykeyboard.donislawdev.com/docs/pack-templates/
# Change anything in it. When you change `id`, rename the file to match,
# booking-dates.toml now, because a pack is found by its file name.
format = 1
[pack]
id = "booking-dates"
name = "Booking dates"
description = "Dates and times for a booking form: hours the clocks skip or repeat, a stay past midnight and a date far ahead."
version = "1.0"
updated = 2026-11-10
license = "CC-BY-4.0"
authors = ["Naughty Keyboard"]
language = "en"
tags = ["dates", "time", "booking"]
fields = ["datetime", "date", "time"]
risk = "normal"
[[values]]
id = "spring-forward"
name = "Check-in at an hour the clocks skip"
type = "next-date"
month = 3
week = "last"
weekday = "sunday"
time = "02:30"
breaks = "On the night clocks go forward in the European Union this hour does not exist, so a form moves it to 03:30 without saying so, or fails with a raw error."
expect = "Refused with an explanation, or moved with the new time shown."
since = "1.0"
[[values]]
id = "fall-back"
name = "Check-in at an hour the clocks repeat"
type = "next-date"
month = 10
week = "last"
weekday = "sunday"
time = "02:30"
breaks = "On the night clocks go back in the European Union this hour happens twice, so a booking stored without a zone cannot say which of the two it means, and a stay of one hour can last two."
expect = "The time stored with its zone or offset, or the form asks which of the two hours is meant."
since = "1.0"
[[values]]
id = "start-evening"
name = "Start at six in the evening"
value = "18:00"
breaks = "The first half of a pair, and on its own an ordinary start time. Type it into the start field, then the next value into the end field."
expect = "Accepted."
fields = ["time"]
since = "1.0"
[[values]]
id = "end-next-morning"
name = "End at nine the next morning"
value = "09:00"
breaks = "The second half: an end earlier than the start. A form that compares the two times alone refuses an overnight stay, or saves one that ends before it begins."
expect = "Read as the next morning and shown that way, or refused with a message that asks for the date."
fields = ["time"]
since = "1.0"
[[values]]
id = "far-future"
name = "The last day of the year 9999"
value = "9999-12-31"
breaks = "A date picker, a database column or a library with a narrower range refuses it with a raw error, cuts the year, or saves it and then sorts it before today."
expect = "Refused with a message that names the latest date allowed, or saved and shown as written."
fields = ["date"]
since = "1.0"
[[pairs]]
id = "overnight"
name = "A stay that ends the next morning"
relation = "range"
a = "start-evening"
b = "end-next-morning"
breaks = "A range check that compares times without dates reads the end as earlier than the start, so an overnight booking is refused or saved the wrong way round."
expect = "Accepted as a stay into the next morning, with both dates shown."
since = "1.0"