@konce-pt/angular 0.7.10 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/charts/src/lib/llms.txt +30 -30
  3. package/fesm2022/konce-pt-angular.mjs +1403 -368
  4. package/fesm2022/konce-pt-angular.mjs.map +1 -1
  5. package/grid/src/lib/llms.txt +48 -48
  6. package/map/src/lib/llms.txt +446 -446
  7. package/package.json +1 -1
  8. package/roadmap/src/lib/llms.txt +74 -74
  9. package/src/lib/accordion/llms.txt +9 -9
  10. package/src/lib/alert/llms.txt +10 -10
  11. package/src/lib/app-shell/llms.txt +59 -59
  12. package/src/lib/auth/llms.txt +104 -0
  13. package/src/lib/autocomplete/llms.txt +8 -8
  14. package/src/lib/avatar/llms.txt +9 -9
  15. package/src/lib/avatar-group/llms.txt +8 -8
  16. package/src/lib/badge/llms.txt +9 -9
  17. package/src/lib/bottom-sheet/llms.txt +14 -14
  18. package/src/lib/breadcrumb/llms.txt +7 -7
  19. package/src/lib/breakpoint/llms.txt +21 -21
  20. package/src/lib/button/llms.txt +34 -34
  21. package/src/lib/button-group/llms.txt +5 -5
  22. package/src/lib/card/llms.txt +27 -27
  23. package/src/lib/carousel/llms.txt +11 -11
  24. package/src/lib/checkbox/llms.txt +7 -7
  25. package/src/lib/chip/llms.txt +6 -6
  26. package/src/lib/chips-input/llms.txt +9 -9
  27. package/src/lib/color-picker/llms.txt +7 -7
  28. package/src/lib/confirm/llms.txt +11 -11
  29. package/src/lib/context-menu/llms.txt +10 -10
  30. package/src/lib/data-table/llms.txt +170 -170
  31. package/src/lib/data-view/llms.txt +8 -8
  32. package/src/lib/date-range/llms.txt +25 -25
  33. package/src/lib/datepicker/llms.txt +48 -48
  34. package/src/lib/dialog/llms.txt +62 -62
  35. package/src/lib/divider/llms.txt +9 -9
  36. package/src/lib/drawer/llms.txt +18 -18
  37. package/src/lib/empty/llms.txt +9 -9
  38. package/src/lib/fab/llms.txt +12 -12
  39. package/src/lib/fieldset/llms.txt +9 -9
  40. package/src/lib/file-upload/llms.txt +10 -10
  41. package/src/lib/form-field/llms.txt +45 -45
  42. package/src/lib/galleria/llms.txt +7 -7
  43. package/src/lib/i18n/llms.txt +21 -21
  44. package/src/lib/icon/llms.txt +20 -20
  45. package/src/lib/icon-button/llms.txt +8 -8
  46. package/src/lib/image/llms.txt +9 -9
  47. package/src/lib/input/llms.txt +27 -25
  48. package/src/lib/input-mask/llms.txt +10 -10
  49. package/src/lib/input-number/llms.txt +34 -34
  50. package/src/lib/input-otp/llms.txt +11 -11
  51. package/src/lib/knob/llms.txt +14 -14
  52. package/src/lib/listbox/llms.txt +8 -8
  53. package/src/lib/megamenu/llms.txt +13 -13
  54. package/src/lib/menu/llms.txt +26 -26
  55. package/src/lib/menubar/llms.txt +10 -10
  56. package/src/lib/meter-group/llms.txt +8 -8
  57. package/src/lib/order-list/llms.txt +8 -8
  58. package/src/lib/paginator/llms.txt +12 -12
  59. package/src/lib/panel/llms.txt +12 -12
  60. package/src/lib/password/llms.txt +12 -9
  61. package/src/lib/pick-list/llms.txt +8 -8
  62. package/src/lib/popover/llms.txt +8 -8
  63. package/src/lib/progress/llms.txt +6 -6
  64. package/src/lib/radio-group/llms.txt +5 -5
  65. package/src/lib/rating/llms.txt +6 -6
  66. package/src/lib/rich-text/llms.txt +51 -32
  67. package/src/lib/scroll-top/llms.txt +7 -7
  68. package/src/lib/select/llms.txt +26 -26
  69. package/src/lib/sidenav/llms.txt +24 -24
  70. package/src/lib/skeleton/llms.txt +6 -6
  71. package/src/lib/slider/llms.txt +17 -17
  72. package/src/lib/speed-dial/llms.txt +7 -7
  73. package/src/lib/spinner/llms.txt +5 -5
  74. package/src/lib/split-button/llms.txt +15 -15
  75. package/src/lib/splitter/llms.txt +11 -11
  76. package/src/lib/stepper/llms.txt +10 -10
  77. package/src/lib/switch/llms.txt +10 -10
  78. package/src/lib/switch-group/llms.txt +16 -16
  79. package/src/lib/tabs/llms.txt +10 -10
  80. package/src/lib/textarea/llms.txt +5 -5
  81. package/src/lib/timeline/llms.txt +10 -10
  82. package/src/lib/toast/llms.txt +10 -10
  83. package/src/lib/toolbar/llms.txt +31 -31
  84. package/src/lib/tooltip/llms.txt +21 -21
  85. package/src/lib/tree/llms.txt +6 -6
  86. package/types/konce-pt-angular.d.ts +316 -7
  87. package/types/konce-pt-angular.d.ts.map +1 -1
@@ -1,153 +1,153 @@
1
1
  # KptMap (kpt-map)
2
2
 
3
- Mapa z konfigurowalnymi znacznikami: kategorie z kolorem i ikoną, grupowanie z licznikiem,
4
- popup szczegółów o szablonie zależnym od kategorii, gotowa karta miejsca, legenda, wyszukiwarka
5
- adresów i dodawanie lokalizacji prawym przyciskiem myszy. Domyślny silnik to Leaflet + OpenStreetMap; API komponentu
6
- nie zna silnika, więc drugi adapter (Google Maps) podmienia się samym providerem.
3
+ A map with configurable markers: categories carrying a color and an icon, clustering with a count,
4
+ a details popup whose template depends on the category, a ready-made place card, a legend, an address
5
+ search and adding locations with the right mouse button. The default engine is Leaflet + OpenStreetMap; the component's API
6
+ knows nothing about the engine, so a second adapter (Google Maps) swaps in through the provider alone.
7
7
 
8
8
  Import: `import { KptMap, KptMapPoiCard, KptMapSearch } from '@konce-pt/angular/map';`
9
9
 
10
- ## Wymagania (aplikacja)
10
+ ## Requirements (the application)
11
11
 
12
- Poniżej domyślny silnik, Leaflet. Alternatywa — Google Maps — nie wymaga żadnej paczki
13
- ani arkusza, za to wymaga klucza API i `mapId`; patrz sekcja „Silniki".
12
+ What follows is for the default engine, Leaflet. The alternative — Google Maps — needs no package
13
+ and no stylesheet, but it does need an API key and a `mapId`; see the "Engines" section.
14
14
 
15
- 1. Zależność **opcjonalna** `leaflet` — biblioteka jej nie instaluje:
15
+ 1. The **optional** `leaflet` dependency — the library does not install it:
16
16
 
17
17
  pnpm add leaflet
18
18
 
19
- 2. Styl Leafleta — jak `@angular/cdk/overlay-prebuilt.css`, przez `angular.json → styles[]`,
20
- NIE jako `import` w `main.ts` (Angular CLI zrobiłby z tego osierocony chunk):
19
+ 2. Leaflet's stylesheet — like `@angular/cdk/overlay-prebuilt.css`, through `angular.json → styles[]`,
20
+ NOT as an `import` in `main.ts` (the Angular CLI would turn that into an orphaned chunk):
21
21
 
22
22
  node_modules/leaflet/dist/leaflet.css
23
23
 
24
24
  Vite/webpack: `import 'leaflet/dist/leaflet.css';`
25
25
 
26
- 3. Provider silnika w bootstrapie:
26
+ 3. The engine provider at bootstrap:
27
27
 
28
28
  bootstrapApplication(App, { providers: [provideKptLeafletMap()] });
29
29
 
30
- Bez zainstalowanej paczki `leaflet` komponent kompiluje się i importuje bez błędu —
31
- dopiero próba wyrenderowania mapy kończy się czytelnym komunikatem z instrukcją.
32
- Leaflet ładujemy dynamicznym `import()`, więc nie obciąża aplikacji, które mapy nie używają.
30
+ Without the `leaflet` package installed the component still compiles and imports without error —
31
+ only an attempt to render the map ends with a readable message and instructions.
32
+ Leaflet is loaded with a dynamic `import()`, so it does not weigh down applications that use no map.
33
33
 
34
- ## Selektor
34
+ ## Selector
35
35
 
36
- `kpt-map` — element blokowy. Wysokość ustawia `[height]` (domyślnie `480px`).
36
+ `kpt-map` — a block element. The height is set by `[height]` (default `480px`).
37
37
 
38
- ## Wejścia
38
+ ## Inputs
39
39
 
40
- - `locations`: `readonly KptMapLocation<T>[]` — **wymagane**
41
- - `categories`: `readonly KptMapCategory[]` — **wymagane**
42
- - `country`: `string` = `'PL'` — preset widoku startowego; nieznany kod cofa się do Polski
43
- - `view`: `{ center, zoom } | null` — jawny widok; nadpisuje `country`
44
- - `bounds`: `KptMapBounds | null` — jawny zasięg; nadpisuje `country` i `view`
45
- - `restrictToCountry`: `boolean` = `false` — blokuje przesuwanie poza zasięg kraju/`bounds`
40
+ - `locations`: `readonly KptMapLocation<T>[]` — **required**
41
+ - `categories`: `readonly KptMapCategory[]` — **required**
42
+ - `country`: `string` = `'PL'` — the initial view preset; an unknown code falls back to Poland
43
+ - `view`: `{ center, zoom } | null` — an explicit view; overrides `country`
44
+ - `bounds`: `KptMapBounds | null` — explicit bounds; override `country` and `view`
45
+ - `restrictToCountry`: `boolean` = `false` — blocks panning outside the country's bounds/`bounds`
46
46
  - `cluster`: `boolean` = `true`
47
- - `clusterOptions`: `{ gridSize?, maxZoom?, minPoints? }` — domyślnie `60 / 16 / 2`
48
- - `clusterColorBy`: `'dominant' | 'none'` = `'none'` — czy grupa bierze kolor najliczniejszej kategorii
49
- - `legend`: `boolean` = `true` — renderuje `kpt-map-legend` pod mapą
50
- - `legendCounts`: `boolean` = `true` — liczniki przy pozycjach legendy
47
+ - `clusterOptions`: `{ gridSize?, maxZoom?, minPoints? }` — `60 / 16 / 2` by default
48
+ - `clusterColorBy`: `'dominant' | 'none'` = `'none'` — whether a cluster takes the color of its most numerous category
49
+ - `legend`: `boolean` = `true` — renders `kpt-map-legend` below the map
50
+ - `legendCounts`: `boolean` = `true` — counters next to the legend entries
51
51
  - `showUserLocation`: `boolean` = `false`
52
- - `followUser`: `boolean` = `false` — centruje mapę przy każdej nowej pozycji
53
- - `allowCreate`: `boolean` = `false` — menu „Dodaj lokalizację" pod prawym przyciskiem
54
- - `search`: `boolean` = `false` — wyszukiwarka adresów w rogu mapy; wymaga portu
55
- `KPT_MAP_GEOCODER` **z metodą `search()`**, bez niego pole się nie renderuje
56
- - `searchPlaceholder`: `string` = `''` — pusty oddaje głos słownikowi
57
- - `searchOptions`: `{ minLength?, debounce?, limit?, countryCodes? }` — domyślnie `3 / 300 / 5`
58
- - `searchZoom`: `number` = `16` — zoom dla wyniku bez własnego zasięgu
59
- - `searchMarker`: `boolean` = `true` — tymczasowa pinezka w znalezionym punkcie
60
- - `geocoder`: `KptMapGeocoderPort | null` = `null` — geokoder **tej** mapy; wygrywa nad
61
- `KPT_MAP_GEOCODER` z DI i obsługuje oba kierunki: wyszukiwarkę i podpowiadanie adresu
62
- w formularzu dodawania
63
- - `popupLayout`: `'plain' | 'card'` = `'plain'` — `card` oddaje popup w całości treści:
64
- bez paddingu, szerszy (`--kpt-map-popup-width-card`), z przewijaniem po przekroczeniu
65
- `--kpt-map-popup-max-height` i bez wbudowanego krzyżyka (daje go karta)
66
- - `distanceFrom`: `KptLatLng | null` = `null` — punkt odniesienia dystansu podawanego
67
- szablonowi popupu; `null` znaczy „pozycja użytkownika z `KPT_MAP_GEOLOCATION`"
52
+ - `followUser`: `boolean` = `false` — recenters the map on every new position
53
+ - `allowCreate`: `boolean` = `false` — an "Add location" menu under the right mouse button
54
+ - `search`: `boolean` = `false` — the address search in the map's corner; it requires the
55
+ `KPT_MAP_GEOCODER` port **with a `search()` method**, and without it the field does not render
56
+ - `searchPlaceholder`: `string` = `''` — an empty value hands it over to the dictionary
57
+ - `searchOptions`: `{ minLength?, debounce?, limit?, countryCodes? }` — `3 / 300 / 5` by default
58
+ - `searchZoom`: `number` = `16` — the zoom for a result with no bounds of its own
59
+ - `searchMarker`: `boolean` = `true` — a temporary pin at the found point
60
+ - `geocoder`: `KptMapGeocoderPort | null` = `null` — **this** map's geocoder; it wins over
61
+ `KPT_MAP_GEOCODER` from DI and serves both directions: the search and the address suggestions
62
+ in the create form
63
+ - `popupLayout`: `'plain' | 'card'` = `'plain'` — `card` hands the popup over to the content entirely:
64
+ no padding, wider (`--kpt-map-popup-width-card`), scrolling once it exceeds
65
+ `--kpt-map-popup-max-height`, and no built-in close button (the card supplies one)
66
+ - `distanceFrom`: `KptLatLng | null` = `null` — the reference point for the distance handed
67
+ to the popup template; `null` means "the user's position from `KPT_MAP_GEOLOCATION`"
68
68
  - `height`: `string` = `'480px'`
69
- - `tileUrl`: `string | null` — domyślnie kafle OSM (patrz „Kafle" niżej); **bez znaczenia dla silnika Google**
70
- - `attribution`: `string | null` — domyślnie `© OpenStreetMap contributors`; jw.
71
- - `engine`: `KptMapEngineFactory | null` — silnik tej mapy; wygrywa nad `KPT_MAP_ENGINE` z DI
72
- (patrz „Silniki")
69
+ - `tileUrl`: `string | null` — OSM tiles by default (see "Tiles" below); **irrelevant for the Google engine**
70
+ - `attribution`: `string | null` — `© OpenStreetMap contributors` by default; likewise
71
+ - `engine`: `KptMapEngineFactory | null` — this map's engine; it wins over `KPT_MAP_ENGINE` from DI
72
+ (see "Engines")
73
73
 
74
- ## Model / zdarzenia
74
+ ## Model / events
75
75
 
76
- - `hiddenCategories`: `model<string[]>` — identyfikatory kategorii ukrytych w legendzie
76
+ - `hiddenCategories`: `model<string[]>` — the ids of categories hidden in the legend
77
77
  - `locationClick`: `KptMapLocation<T>`
78
78
  - `clusterClick`: `KptMapCluster<T>`
79
- - `locationCreate`: `{ at, categoryId, value }` — komponent **nie** dopisuje nic do `locations`;
80
- źródłem prawdy jest aplikacja
81
- - `searchSelect`: `KptMapSearchResult` — wybrano podpowiedź; mapa **sama** przeniosła już widok
82
- - `viewChange`: `{ center, zoom, bounds }` — po zakończeniu ruchu mapy
79
+ - `locationCreate`: `{ at, categoryId, value }` — the component appends **nothing** to `locations`;
80
+ the application is the source of truth
81
+ - `searchSelect`: `KptMapSearchResult` — a suggestion was picked; the map has **already** moved the view itself
82
+ - `viewChange`: `{ center, zoom, bounds }` — after the map's movement ends
83
83
  - `userPositionChange`: `KptMapUserPosition | null`
84
84
  - `userLocationError`: `KptMapGeolocationError`
85
85
 
86
86
  ## KptMapCategory
87
87
 
88
- { id: 'closure', label: 'Zamknięcie', color: 'danger', icon: 'lock',
88
+ { id: 'closure', label: 'Closure', color: 'danger', icon: 'lock',
89
89
  shape: 'pin', legend: true, fields: [ … ] }
90
90
 
91
- `color` to rola z warstwy semantycznej (`primary | success | warning | danger | info | muted`)
92
- albo gotowe `var(--kpt-*)`. Literały (`#c00`, `rgb(...)`) są zablokowane na poziomie typu —
93
- kontrakt repo wymaga tokenów.
91
+ `color` is a role from the semantic layer (`primary | success | warning | danger | info | muted`)
92
+ or a ready `var(--kpt-*)`. Literals (`#c00`, `rgb(...)`) are blocked at the type level —
93
+ the repo's contract requires tokens.
94
94
 
95
- Rola daje **trzy** zmienne, zależnie od tego, czym element jest:
95
+ A role yields **three** variables, depending on what the element is:
96
96
 
97
- | funkcja | do czego | `muted` daje |
97
+ | function | for what | `muted` gives |
98
98
  |---|---|---|
99
- | `colorVarOf()` | tło (pinezka, krążek legendy, grupa) | `--kpt-color-muted` (neutral-100) |
100
- | `contrastVarOf()` | glif **na** tym tle | `--kpt-color-on-muted` (neutral-800) |
101
- | `foregroundVarOf()` | glif stojący wprost na powierzchni, bez tła | `--kpt-color-on-surface-muted` |
99
+ | `colorVarOf()` | a background (a pin, a legend dot, a cluster) | `--kpt-color-muted` (neutral-100) |
100
+ | `contrastVarOf()` | a glyph **on** that background | `--kpt-color-on-muted` (neutral-800) |
101
+ | `foregroundVarOf()` | a glyph standing straight on the surface, with no background | `--kpt-color-on-surface-muted` |
102
102
 
103
- Pomyłka boli tylko przy `muted`, bo to jedyna rola, której `--kpt-color-*` jest kolorem tła,
104
- a nie treści — pozostałe są nasycone i czytają się w obu funkcjach. Stąd trzecia funkcja:
105
- karta miejsca maluje ikonę kategorii wprost na tle karty i `colorVarOf()` dałby jej
106
- neutral-100 na bieli. `icon` to nazwa z `KptIconRegistry` (pełny Tabler dodaje
107
- `provideKptTablerIcons()`). `shape`: `pin` (domyślnie, kotwiczy czubkiem), `dot`, `square`.
103
+ Getting it wrong only hurts with `muted`, because it is the only role whose `--kpt-color-*` is a background
104
+ color rather than a content one — the rest are saturated and read fine in either function. Hence the third function:
105
+ the place card paints the category icon straight onto the card's background, and `colorVarOf()` would give it
106
+ neutral-100 on white. `icon` is a name from `KptIconRegistry` (the full Tabler set comes with
107
+ `provideKptTablerIcons()`). `shape`: `pin` (the default, anchored at the tip), `dot`, `square`.
108
108
 
109
109
  ## KptMapLocation
110
110
 
111
111
  { id: 'w1', at: { lat: 51.11, lng: 17.04 }, categoryId: 'opening',
112
112
  title: '2210 · Wrocław', data: { … } }
113
113
 
114
- `title` jest wymagany — trafia do `aria-label` znacznika. `data` jest dowolne i trafia
115
- do szablonu popupu.
114
+ `title` is required — it goes into the marker's `aria-label`. `data` is arbitrary and is handed
115
+ to the popup template.
116
116
 
117
- ## Szablony
117
+ ## Templates
118
118
 
119
- `kptMapPopup` — popup szczegółów, wybierany po kategorii. Szablon bez atrybutu `category`
120
- jest wariantem zapasowym; bez żadnego szablonu komponent pokazuje tytuł i nazwę kategorii.
119
+ `kptMapPopup` — the details popup, chosen by category. A template with no `category` attribute
120
+ is the fallback; with no template at all the component shows the title and the category name.
121
121
 
122
122
  <kpt-map [locations]="locations()" [categories]="categories">
123
123
  <ng-template kptMapPopup category="closure" let-loc let-d="data">
124
124
  <strong>{{ loc.title }}</strong>
125
- <p>Zamknięcie · {{ d.status }}</p>
126
- <kpt-button size="sm" (click)="openCard(loc)">Otwórz kartę projektu</kpt-button>
125
+ <p>Closure · {{ d.status }}</p>
126
+ <kpt-button size="sm" (click)="openCard(loc)">Open the project card</kpt-button>
127
127
  </ng-template>
128
128
  <ng-template kptMapPopup let-loc><strong>{{ loc.title }}</strong></ng-template>
129
129
  </kpt-map>
130
130
 
131
- Kontekst: `$implicit` = lokalizacja, `data` = `location.data` (typ `T | undefined`, bo `data`
132
- jest opcjonalne), `category`, `distance`, `close()`.
131
+ Context: `$implicit` = the location, `data` = `location.data` (typed `T | undefined`, because `data`
132
+ is optional), `category`, `distance`, `close()`.
133
133
 
134
- `distance` to odległość punktu w **metrach** od `[distanceFrom]`, a bez niego od pozycji
135
- użytkownika. `null`, gdy nie ma punktu odniesienia — geolokalizacja wyłączona albo odmówiona.
136
- Formatuje się ją `formatDistance(metry, locale)` z rdzenia; `kpt-map-poi-card` robi to sam.
134
+ `distance` is the point's distance in **meters** from `[distanceFrom]`, or from the user's position
135
+ without it. `null` when there is no reference point — geolocation is disabled or was refused.
136
+ Format it with `formatDistance(meters, locale)` from the core; `kpt-map-poi-card` does it itself.
137
137
 
138
- Typ `data` domyślnie jest luźny — Angular nie przenosi generyka z `kpt-map` na dyrektywę osadzoną
139
- w jego treści. Kto chce wnioskowania, wiąże `[kptMapPopupTypeFor]` z tą samą tablicą, którą karmi
140
- mapę (input istnieje wyłącznie po to, w runtime nieużywany):
138
+ The type of `data` is loose by default — Angular does not carry the generic from `kpt-map` onto a directive
139
+ embedded in its content. If you want inference, bind `[kptMapPopupTypeFor]` to the same array you feed
140
+ the map (the input exists solely for that and is unused at runtime):
141
141
 
142
142
  <ng-template kptMapPopup [kptMapPopupTypeFor]="locations()" category="opening" let-d="data">
143
143
  <p>{{ d?.address }}</p>
144
144
  </ng-template>
145
145
 
146
- Popup otwarty przy krawędzi kontenera przesuwa mapę tak, żeby się zmieścił (auto-pan mierzy go
147
- po pierwszym renderze — przed nim nie znamy jego wymiarów).
146
+ A popup opened near the container's edge pans the map so that it fits (auto-pan measures it
147
+ after the first render — before that we do not know its dimensions).
148
148
 
149
- `kptMapCreateForm` — własny formularz dodawania. Jego obecność wyłącza formularz
150
- budowany ze schemy kategorii.
149
+ `kptMapCreateForm` — your own create form. Its presence disables the form built
150
+ from the category schema.
151
151
 
152
152
  <ng-template kptMapCreateForm let-api>
153
153
  <!-- api: { at, category, close(), submit(value) } -->
@@ -155,9 +155,9 @@ budowany ze schemy kategorii.
155
155
 
156
156
  ## KptMapPoiCard (kpt-map-poi-card)
157
157
 
158
- Gotowa karta miejsca do popupu: dystans, zdjęcie 16:9, przycisk główny + udostępnij + ulubione,
159
- wiersz kategorii, rozwijane godziny otwarcia ze statusem otwarte/zamknięte oraz wiersze
160
- kontaktowe (telefon, www, e-mail, współrzędne) z akcjami i kopiowaniem do schowka.
158
+ A ready-made place card for the popup: the distance, a 16:9 photo, a primary button + share + favorite,
159
+ a category row, expandable opening hours with an open/closed status, and contact
160
+ rows (phone, website, e-mail, coordinates) with actions and copy-to-clipboard.
161
161
 
162
162
  <kpt-map [locations]="places()" [categories]="categories" popupLayout="card">
163
163
  <ng-template kptMapPopup [kptMapPopupTypeFor]="places()"
@@ -167,139 +167,139 @@ kontaktowe (telefon, www, e-mail, współrzędne) z akcjami i kopiowaniem do sch
167
167
  [poi]="poiOf(loc, d)"
168
168
  [distance]="distance"
169
169
  [category]="category ?? null"
170
- primaryLabel="Wyznacz trasę"
170
+ primaryLabel="Get directions"
171
171
  (primaryAction)="planRoute($event)"
172
172
  (closed)="close()"
173
173
  />
174
174
  </ng-template>
175
175
  </kpt-map>
176
176
 
177
- Wejścia:
177
+ Inputs:
178
178
 
179
- - `poi`: `KptMapPoi` — **wymagane**
180
- - `distance`: `number | null` = `null` — metry; `null` chowa wiersz dystansu
181
- - `category`: `KptMapCategory | null` = `null` — ikona, kolor i etykieta wiersza kategorii
182
- - `primaryLabel`: `string` = `''` — pusta chowa przycisk główny
179
+ - `poi`: `KptMapPoi` — **required**
180
+ - `distance`: `number | null` = `null` — meters; `null` hides the distance row
181
+ - `category`: `KptMapCategory | null` = `null` — the icon, the color and the label of the category row
182
+ - `primaryLabel`: `string` = `''` — an empty value hides the primary button
183
183
  - `showShare`, `showFavourite`, `closable`: `boolean` = `true`
184
184
  - `favourite`: `model<boolean>` = `false`
185
185
  - `hoursExpanded`: `model<boolean>` = `false`
186
186
 
187
- Wyjścia: `primaryAction: KptMapPoi`, `closed: void`, `shared: KptMapPoi`.
187
+ Outputs: `primaryAction: KptMapPoi`, `closed: void`, `shared: KptMapPoi`.
188
188
 
189
- `KptMapPoi` jest **osobne od modelu domenowego** — mapowanie `location.data → KptMapPoi` robi
190
- aplikacja. Biblioteka nie zgaduje, które pole jest telefonem:
189
+ `KptMapPoi` is **separate from the domain model** — the `location.data → KptMapPoi` mapping is done by
190
+ the application. The library does not guess which field is the phone number:
191
191
 
192
192
  { title, address?, photo?, photoAlt?, hours?, phone?, email?, website?, at?,
193
193
  facts?: [{ icon?, label, value }] }
194
194
 
195
- `photo` przyjmuje adres albo `data:` — karta nie rozróżnia. Bez niego rysuje zastępnik z ikoną,
196
- zamiast zwijać kadr do zera. `facts` to dowolne wiersze „ikona · etykieta · wartość" pod kontaktami.
197
-
198
- Zachowania warte zapamiętania:
199
-
200
- - **Zdjęcie** jest w kadrze 16:9 z `object-fit: cover` na zwykłym `<img>`, nie na `kpt-image` —
201
- tamten jest `inline-block` z `height: auto` i kadru nie utrzyma.
202
- - **Godziny** to natywny disclosure (`aria-expanded` + `aria-controls`), nie `kpt-accordion`:
203
- `KptAccordionPanel.title` przyjmuje sam string, a nagłówek musi zmieścić ikonę, status
204
- w kolorze i chevron. Status liczy `isOpenAt()`; dni sklejają się przez `groupWeek()`.
205
- - **Akcje wierszy** są zawsze w DOM. Ukrywa je wyłącznie `@media (hover: hover) and
206
- (pointer: fine)`, a `:hover`/`:focus-within` je odsłania — na dotyku i z klawiatury
207
- są dostępne bez sztuczek.
208
- - **Kopiowanie** idzie przez `navigator.clipboard`; bez tego API przycisk się nie renderuje.
209
- Po sukcesie ikona zmienia się na `check`, a komunikat leci do `aria-live`.
210
- - **Udostępnianie** woła `navigator.share()`, a bez niego kopiuje adres. Anulowanie arkusza
211
- przez użytkownika nie jest błędem i nie emituje `shared`.
212
- - **Tytuł to `<p role="heading" aria-level="3">`, nie `<h3>`.** Style komponentów siedzą
213
- w `@layer kpt.components`, a CSS aplikacji spoza warstw wygrywa z nimi zawsze — zwykły
214
- `<h3>` łapałby każdą regułę `h3 { … }` z układu strony (w playgroundzie robiła z tytułu
215
- miejsca 13-pikselowy uppercase). Dla czytnika ekranu to dalej nagłówek poziomu 3.
216
- - Karta nie ustawia własnego `role` na hoście — popup `kpt-map` jest już `role="dialog"`.
217
-
218
- Anatomia i przewijanie:
195
+ `photo` takes a URL or a `data:` string — the card does not tell them apart. Without one it draws a placeholder
196
+ with an icon instead of collapsing the frame to zero. `facts` are arbitrary "icon · label · value" rows below the contacts.
197
+
198
+ Behaviors worth remembering:
199
+
200
+ - **The photo** sits in a 16:9 frame with `object-fit: cover` on a plain `<img>`, not on `kpt-image` —
201
+ that one is `inline-block` with `height: auto` and will not hold the frame.
202
+ - **The hours** are a native disclosure (`aria-expanded` + `aria-controls`), not a `kpt-accordion`:
203
+ `KptAccordionPanel.title` accepts a plain string, while the header has to fit an icon, a colored
204
+ status and a chevron. The status is computed by `isOpenAt()`; days are joined by `groupWeek()`.
205
+ - **The row actions** are always in the DOM. They are hidden only by `@media (hover: hover) and
206
+ (pointer: fine)`, and `:hover`/`:focus-within` reveals them — on touch and from the keyboard
207
+ they are available without any tricks.
208
+ - **Copying** goes through `navigator.clipboard`; without that API the button does not render.
209
+ On success the icon changes to `check`, and a message goes to `aria-live`.
210
+ - **Sharing** calls `navigator.share()`, and without it copies the address. The user canceling the sheet
211
+ is not an error and emits no `shared`.
212
+ - **The title is a `<p role="heading" aria-level="3">`, not an `<h3>`.** Component styles live
213
+ in `@layer kpt.components`, and an application's CSS outside any layer always beats them — a plain
214
+ `<h3>` would catch every `h3 { … }` rule from the page's layout (in the playground that turned the place title
215
+ into 13-pixel uppercase). To a screen reader it is still a level 3 heading.
216
+ - The card does not set its own `role` on the host — the `kpt-map` popup is already `role="dialog"`.
217
+
218
+ Anatomy and scrolling:
219
219
 
220
220
  kpt-map-poi-card
221
- .kpt-map-poi__head tytuł + krzyżyk — nieruchome
222
- .kpt-map-poi__meta dystans + adres — nieruchome
223
- .kpt-map-poi__scroll zdjęcie, akcje, kategoria, godziny, kontakty, aria-live
221
+ .kpt-map-poi__head title + close button — fixed
222
+ .kpt-map-poi__meta distance + address — fixed
223
+ .kpt-map-poi__scroll photo, actions, category, hours, contacts, aria-live
224
224
 
225
- Nagłówek stoi, przewija się wyłącznie `__scroll`. Dystans i adres należą do bloku
226
- nagłówkowego, bo są częścią tożsamości miejsca — żeby przewijały się razem z treścią,
227
- wystarczy przenieść `__meta` do `__scroll`.
225
+ The header stays put, and only `__scroll` scrolls. The distance and the address belong to the header
226
+ block, because they are part of the place's identity — to make them scroll with the content,
227
+ just move `__meta` into `__scroll`.
228
228
 
229
- Warunkiem jest to, że `popupLayout="card"` **oddaje przewijanie karcie**: `.kpt-map__popup-body`
230
- dostaje wtedy `display: flex` i `overflow: hidden`, a karta pełną wysokość popupu. Dopóki
231
- przewija kontener nadrzędny, nagłówka nie da się przypiąć bez `sticky` i ujemnych marginesów.
232
- Sam popup nie dostaje `overflow: hidden` — strzałka `::after` wisi na `top: 100%` i zostałaby
233
- ucięta. Karta poza popupem (stories, dowolny wrapper bez limitu wysokości) nie przewija się
234
- wcale: `__scroll` ma `flex: 0 1 auto` i bez sufitu wysokości po prostu rośnie.
229
+ The condition for this is that `popupLayout="card"` **hands scrolling to the card**: `.kpt-map__popup-body`
230
+ then gets `display: flex` and `overflow: hidden`, and the card the popup's full height. As long as
231
+ the parent container scrolls, the header cannot be pinned without `sticky` and negative margins.
232
+ The popup itself does not get `overflow: hidden` — the `::after` arrow hangs at `top: 100%` and would be
233
+ cut off. A card outside a popup (stories, any wrapper with no height limit) does not scroll
234
+ at all: `__scroll` has `flex: 0 1 auto` and simply grows without a height ceiling.
235
235
 
236
- ## Dodawanie lokalizacji
236
+ ## Adding locations
237
237
 
238
- Prawy klik → menu z kategoriami → `kpt-dialog` z formularzem. Domyślnie formularz powstaje
239
- z `category.fields` na Signal Forms. Typy pól i kontrolki:
238
+ Right-click → a menu of categories → a `kpt-dialog` with a form. By default the form is built
239
+ from `category.fields` on Signal Forms. Field types and controls:
240
240
 
241
- | `type` | kontrolka | uwagi |
241
+ | `type` | control | notes |
242
242
  |---|---|---|
243
243
  | `text` | `kpt-input` | |
244
244
  | `textarea` | `kpt-textarea` | |
245
- | `number` | `kpt-input-number` | `min`/`max` ze schemy idą przez walidatory, nie przez `[min]` |
246
- | `date` | `kpt-datepicker` | `min`/`max` jako daty ISO |
247
- | `select` / `multiselect` | `kpt-select` (`multiple`) | wymaga `options` |
248
- | `checkbox` | `kpt-checkbox` | `placeholder` służy za etykietę przy polu |
249
- | `tel` | `kpt-input type="tel"` | bez walidacji formatu — numery bywają dziwne |
250
- | `email` | `kpt-input type="email"` | walidator `email()` |
251
- | `url` | `kpt-input type="url"` | wzorzec `https?://…` |
252
- | `image` | `kpt-map-image-field` | wybór pliku **albo** wklejony adres; wartość to `string` |
253
- | `hours` | `kpt-map-hours-field` | wartość to `KptMapOpeningHours` |
245
+ | `number` | `kpt-input-number` | `min`/`max` from the schema go through validators, not through `[min]` |
246
+ | `date` | `kpt-datepicker` | `min`/`max` as ISO dates |
247
+ | `select` / `multiselect` | `kpt-select` (`multiple`) | requires `options` |
248
+ | `checkbox` | `kpt-checkbox` | `placeholder` serves as the label next to the field |
249
+ | `tel` | `kpt-input type="tel"` | no format validation — phone numbers can be odd |
250
+ | `email` | `kpt-input type="email"` | the `email()` validator |
251
+ | `url` | `kpt-input type="url"` | the `https?://…` pattern |
252
+ | `image` | `kpt-map-image-field` | a file picker **or** a pasted URL; the value is a `string` |
253
+ | `hours` | `kpt-map-hours-field` | the value is a `KptMapOpeningHours` |
254
254
 
255
- Współrzędne z kliknięcia są pokazane, ale nieedytowalne.
255
+ The coordinates from the click are shown but not editable.
256
256
 
257
- `image` czyta plik `FileReader.readAsDataURL()` i **nigdzie go nie wysyła** — biblioteka nie ma
258
- wiedzy o storage'u aplikacji. Limit 2 MB jest realny, nie ozdobny: data URL puchnie o ~⅓,
259
- a cała wartość leci w `locationCreate`. Wpisanie adresu czyści wybrany plik i odwrotnie.
257
+ `image` reads the file with `FileReader.readAsDataURL()` and **sends it nowhere** — the library knows
258
+ nothing about the application's storage. The 2 MB limit is real, not decorative: a data URL swells by about a third,
259
+ and the whole value travels in `locationCreate`. Typing a URL clears the chosen file and vice versa.
260
260
 
261
- `hours` daje siedem wierszy od poniedziałku (nazwy dni z `Intl`, nie ze słownika), po dwa
262
- przedziały na dobę, znacznik „zamknięte" i skrót „tak samo przez cały tydzień" kopiujący
263
- poniedziałek na resztę. Dzień nieoznaczony jako zamknięty musi mieć obie godziny — inaczej
264
- formularz jest `invalid`.
261
+ `hours` gives seven rows starting on Monday (day names from `Intl`, not from the dictionary), two
262
+ ranges per day, a "closed" marker and a "same all week" shortcut that copies
263
+ Monday onto the rest. A day not marked as closed must have both times — otherwise
264
+ the form is `invalid`.
265
265
 
266
- Godzinę wybiera się na `kpt-datepicker selectionMode="time"` (tarcza zegara, `minuteStep=5`),
267
- nie na natywnym `<input type="time">` — ten renderuje własny, systemowy dropdown, którego
268
- nie da się ostylować i który wygląda obco pośród kontrolek biblioteki. Wartość jest ta sama:
269
- `HH:mm` albo pusty string.
266
+ The time is picked on a `kpt-datepicker selectionMode="time"` (a clock dial, `minuteStep=5`),
267
+ not on a native `<input type="time">` — that one renders its own system dropdown, which
268
+ cannot be styled and looks alien among the library's controls. The value is the same:
269
+ `HH:mm` or an empty string.
270
270
 
271
- **Ograniczenie:** przy dynamicznej schemie model to `Record<string, unknown>`, więc pola
272
- nie są typowane statycznie. Kto potrzebuje typów — podaje własny `kptMapCreateForm`.
271
+ **A limitation:** with a dynamic schema the model is a `Record<string, unknown>`, so the fields
272
+ are not statically typed. Anyone who needs types supplies their own `kptMapCreateForm`.
273
273
 
274
- Bramka „nie strasz czerwienią świeżego formularza" siedzi w prezentacji (`touched() &&
275
- invalid()`), a nie w walidatorze — zgodnie z kontraktem Signal Forms w tym repo.
274
+ The "do not scare people with red on a fresh form" gate lives in the presentation (`touched() &&
275
+ invalid()`), not in the validator — per this repo's Signal Forms contract.
276
276
 
277
- ## Podpowiadanie adresu (geokodowanie wsteczne)
277
+ ## Address suggestions (reverse geocoding)
278
278
 
279
- Pola formularza „dodaj lokalizację" mogą wypełnić się adresem klikniętego punktu. Dwa warunki:
280
- pole ma `autofill` wskazujące część adresu, a mapa ma port — z DI (`KPT_MAP_GEOCODER`)
281
- albo z wejścia `[geocoder]`, które wygrywa.
279
+ The fields of the "add location" form can fill themselves in with the clicked point's address. Two conditions:
280
+ the field has an `autofill` pointing at a part of the address, and the map has a port — from DI (`KPT_MAP_GEOCODER`)
281
+ or from the `[geocoder]` input, which wins.
282
282
 
283
- { key: 'city', type: 'text', label: 'Miasto', autofill: 'city' },
284
- { key: 'address', type: 'text', label: 'Adres', autofill: 'address' },
283
+ { key: 'city', type: 'text', label: 'City', autofill: 'city' },
284
+ { key: 'address', type: 'text', label: 'Address', autofill: 'address' },
285
285
 
286
- Części adresu (`KptMapAddressPart`): `street`, `houseNumber`, `address` (ulica z numerem),
287
- `city`, `postcode`, `state`, `county`, `country`, `countryCode`, `label` (pełny adres jednym
288
- ciągiem). Działa tylko dla pól `text` i `textarea` — data ani liczba nie przyjmą fragmentu adresu.
286
+ The address parts (`KptMapAddressPart`): `street`, `houseNumber`, `address` (street with number),
287
+ `city`, `postcode`, `state`, `county`, `country`, `countryCode`, `label` (the full address as one
288
+ string). It works only for `text` and `textarea` fields — a date or a number will not take a fragment of an address.
289
289
 
290
- **Podpowiedź nigdy nie nadpisuje tego, co użytkownik wpisał** — wypełniane są wyłącznie pola
291
- puste w chwili odpowiedzi. Brak portu albo nieudane zapytanie nie blokuje formularza: pola
292
- zostają puste, a pod współrzędnymi pojawia się notka.
290
+ **A suggestion never overwrites what the user typed** — only fields that are
291
+ empty at the moment of the response are filled in. A missing port or a failed request does not block the form: the fields
292
+ stay empty, and a note appears under the coordinates.
293
293
 
294
- Gotowy adapter (opt-in, NIE jest rejestrowany domyślnie):
294
+ The ready adapter (opt-in, NOT registered by default):
295
295
 
296
296
  providers: [provideKptLeafletMap(), provideKptNominatimGeocoder({ language: 'pl' })]
297
297
 
298
- **Polityka Nominatim.** Publiczna instancja `nominatim.openstreetmap.org` dopuszcza maksymalnie
299
- jedno zapytanie na sekundę, wymaga rozpoznawalnego `Referer`/`User-Agent` i zabrania masowego
300
- odpytywania; ruch może zostać zablokowany bez uprzedzenia, w szczególności komercyjny. Adapter
301
- sam kolejkuje zapytania z odstępem sekundy i przerywa je przy zamknięciu okna, ale do produkcji
302
- podstaw własną instancję (`baseUrl`) albo komercyjnego dostawcę przez własny `KPT_MAP_GEOCODER`:
298
+ **The Nominatim policy.** The public `nominatim.openstreetmap.org` instance allows at most
299
+ one request per second, requires a recognizable `Referer`/`User-Agent` and forbids bulk
300
+ querying; traffic can be blocked without notice, commercial traffic in particular. The adapter
301
+ queues requests a second apart on its own and aborts them when the dialog closes, but for production
302
+ put your own instance behind it (`baseUrl`) or a commercial provider through your own `KPT_MAP_GEOCODER`:
303
303
 
304
304
  export interface KptMapGeocoderPort {
305
305
  reverse(at: KptLatLng, options?): Observable<KptMapAddress | null>;
@@ -307,425 +307,425 @@ podstaw własną instancję (`baseUrl`) albo komercyjnego dostawcę przez własn
307
307
  resolve?(suggestion: KptMapSuggestion, options?): Observable<KptMapSearchResult | null>;
308
308
  }
309
309
 
310
- Tylko `reverse()` jest obowiązkowe. Bez `search()` mapa nie pokazuje wyszukiwarki;
311
- `resolve()` opisano niżej, przy podpowiedziach bez współrzędnych.
310
+ Only `reverse()` is mandatory. Without `search()` the map shows no search field;
311
+ `resolve()` is described below, with suggestions that carry no coordinates.
312
312
 
313
- **Odstęp między zapytaniami (`minIntervalMs`).** Sekunda nie jest cechą Nominatima, tylko
314
- wymogiem regulaminu **publicznej instancji OSM** — dlatego domyślna zależy od `baseUrl`:
313
+ **The interval between requests (`minIntervalMs`).** The one-second gap is not a property of Nominatim, it is a
314
+ requirement of the **public OSM instance's** terms — which is why the default depends on `baseUrl`:
315
315
 
316
316
  provideKptNominatimGeocoder() // 1000 ms
317
317
  provideKptNominatimGeocoder({ baseUrl: 'https://geo.local' }) // 0 ms
318
318
  provideKptNominatimGeocoder({ baseUrl: '…', minIntervalMs: 200 })
319
319
 
320
- Na własnym serwerze nie ma czego respektować, a przy podpowiadaniu w trakcie pisania sekundowa
321
- zwłoka jest różnicą między „działa" a „zacina się". `0` zdejmuje też kolejkowanie — zapytania
322
- lecą równolegle. Podstawiając **cudzą, współdzieloną** instancję ustaw wartość jawnie.
320
+ On your own server there is nothing to respect, and for as-you-type suggestions a one-second
321
+ delay is the difference between "it works" and "it stutters". `0` also removes the queuing — the requests
322
+ go out in parallel. When pointing at **someone else's shared** instance, set the value explicitly.
323
323
 
324
- Testy i Storybook: `provideKptMapGeocoderStub({ city: 'Warszawa', address: 'Marszałkowska 100' })`.
324
+ Tests and Storybook: `provideKptMapGeocoderStub({ city: 'Warszawa', address: 'Marszałkowska 100' })`.
325
325
 
326
- ## Wyszukiwarka adresów (geokodowanie wprost)
326
+ ## Address search (forward geocoding)
327
327
 
328
- `[search]` pokazuje w prawym górnym rogu mapy pole z podpowiedziami. Wpisanie frazy odpytuje
329
- port geokodera — **ten sam**, co podpowiadanie adresu w formularzu dodawania, tylko
330
- w drugą stronę; z DI (`KPT_MAP_GEOCODER`) albo z wejścia `[geocoder]`, które wygrywa.
331
- Metoda `search()` jest w porcie **opcjonalna**: porty napisane przed jej powstaniem dalej
332
- się kompilują, a mapa bez niej po prostu nie renderuje pola.
328
+ `[search]` puts a field with suggestions in the map's top right corner. Typing a phrase queries
329
+ the geocoder port — the **same** one as the address suggestions in the create form, just
330
+ in the other direction; from DI (`KPT_MAP_GEOCODER`) or from the `[geocoder]` input, which wins.
331
+ The `search()` method is **optional** in the port: ports written before it existed still
332
+ compile, and a map without it simply does not render the field.
333
333
 
334
334
  <kpt-map [locations]="places()" [categories]="categories" search
335
335
  (searchSelect)="onFound($event)" />
336
336
 
337
- // main.ts — bez tego pola nie będzie
337
+ // main.ts — without this there will be no field
338
338
  providers: [provideKptLeafletMap(), provideKptNominatimGeocoder({ language: 'pl' })]
339
339
 
340
- Dwa typy, celowo rozdzielone:
340
+ Two types, deliberately kept apart:
341
341
 
342
- KptMapSuggestion = { id, label, at?, bounds?, address? } // pozycja na liście
343
- KptMapSearchResult = KptMapSuggestion & { at: KptLatLng } // to, co wychodzi ze zdarzenia
342
+ KptMapSuggestion = { id, label, at?, bounds?, address? } // an entry in the list
343
+ KptMapSearchResult = KptMapSuggestion & { at: KptLatLng } // what comes out of the event
344
344
 
345
- `address` to ten sam `KptMapAddress`, co przy geokodowaniu wstecznym — nie ma drugiego modelu adresu.
345
+ `address` is the same `KptMapAddress` as in reverse geocoding — there is no second address model.
346
346
 
347
- ### Podpowiedzi bez współrzędnych (`resolve()`)
347
+ ### Suggestions with no coordinates (`resolve()`)
348
348
 
349
- Większość geokoderów pytanych wprost (Nominatim, Photon, Google Geocoding API) podaje `lat`/`lng`
350
- od razu na liście. **Google Places Autocomplete tego nie robi**: zwraca `placeId` i tekst,
351
- a współrzędne dociąga się osobnym Place Details. To nie jest dziwactwo jednego dostawcy — na tym
352
- stoi jego rozliczanie sesyjne, w którym cały ciąg podpowiedzi plus finalne Place Details liczy się
353
- jako jedno zdarzenie.
349
+ Most geocoders queried forward (Nominatim, Photon, the Google Geocoding API) give `lat`/`lng`
350
+ right in the list. **Google Places Autocomplete does not**: it returns a `placeId` and text,
351
+ and the coordinates come from a separate Place Details call. This is not one provider's quirk — it is what
352
+ its session-based billing rests on, where a whole run of suggestions plus the final Place Details counts
353
+ as one event.
354
354
 
355
- Dlatego `search()` zwraca `KptMapSuggestion` (z `at` **opcjonalnym**), a port może dopisać:
355
+ That is why `search()` returns a `KptMapSuggestion` (with `at` **optional**), and a port may add:
356
356
 
357
357
  resolve?(suggestion: KptMapSuggestion, options?: { language?, signal? })
358
358
  : Observable<KptMapSearchResult | null>;
359
359
 
360
- Reguła jest jednozdaniowa: **jeśli Twój `search()` nie zwraca `at`, musisz zaimplementować
361
- `resolve()`.** Kto zwraca współrzędne od razu, tej metody nie potrzebuje — pole nawet jej nie zawoła.
360
+ The rule fits in one sentence: **if your `search()` does not return `at`, you must implement
361
+ `resolve()`.** Anyone returning coordinates right away does not need the method — the field will not even call it.
362
362
 
363
- Obsługa siedzi w `kpt-map-search`, nie w `kpt-map`, więc gwarancja obowiązuje tak samo przy polu
364
- postawionym samodzielnie:
363
+ The handling lives in `kpt-map-search`, not in `kpt-map`, so the guarantee holds just the same for a field
364
+ standing on its own:
365
365
 
366
- - podpowiedź z `at` → `selected` leci **natychmiast**, bez dodatkowego zapytania;
367
- - podpowiedź bez `at` → `resolve()` raz, **po wybraniu pozycji**, nigdy w trakcie pisania;
368
- panel zostaje otwarty z notą „Szukam…", a `aria-busy` na polu niesie to czytnikowi;
369
- - `null` albo błąd → etykieta zostaje w polu, lista wraca (można wybrać inną pozycję),
370
- a `aria-live` mówi „brak pasujących miejsc";
371
- - kolejny wybór i wyczyszczenie pola przerywają zapytanie w locie.
366
+ - a suggestion with `at` → `selected` fires **immediately**, with no extra request;
367
+ - a suggestion without `at` → `resolve()` once, **after the entry is picked**, never while typing;
368
+ the panel stays open with a "Searching…" note, and `aria-busy` on the field carries that to a screen reader;
369
+ - `null` or an error → the label stays in the field, the list comes back (another entry can be picked),
370
+ and `aria-live` says "no matching places";
371
+ - another pick or clearing the field aborts an in-flight request.
372
372
 
373
- **`selected` i `searchSelect` zawsze niosą współrzędne.** Aplikacja nigdy nie sprawdza, czy `at` jest.
373
+ **`selected` and `searchSelect` always carry coordinates.** The application never checks whether `at` is there.
374
374
 
375
- Gdy port **nie ma** `resolve()`, podpowiedzi bez `at` w ogóle nie trafiają na listę: pozycja,
376
- której kliknięcie nie miałoby dokąd polecieć, jest gorsza niż jej brak.
375
+ When a port **has no** `resolve()`, suggestions without `at` never reach the list at all: an entry whose
376
+ click would have nowhere to fly is worse than no entry.
377
377
 
378
- Atrapa umie udawać takiego dostawcę — bez tego ścieżki nie da się pokazać bez klucza API:
378
+ The stub can pretend to be such a provider — without it there is no way to demo the path without an API key:
379
379
 
380
380
  provideKptMapGeocoderStub(address, results, { deferCoordinates: true })
381
381
 
382
- **Po wyborze podpowiedzi mapa przenosi widok sama.** Wynik z `bounds` (miasto, region,
383
- województwo) dostaje `fitBounds`, a wynik bez nich — `setView` na `[searchZoom]`. Zdarzenie
384
- `searchSelect` leci już po ruchu i służy aplikacji do jej własnych rzeczy (przestawienie
385
- `[distanceFrom]`, zapytanie o punkty w okolicy), nie do nawigacji.
386
-
387
- Adapter Nominatim **celowo odrzuca zbyt ciasny prostokąt** i zwraca wtedy sam punkt.
388
- Pojedynczy adres ma tam bounding box rzędu 0,0001°, więc `fitBounds` skakałby na nim do
389
- górnego limitu zoomu i każdy adres lądowałby w innym przybliżeniu, zależnym od tego, jak
390
- dostawca obrysował budynek — z progiem adresy trafiają w powtarzalne `[searchZoom]`,
391
- a miasta dalej wchodzą w kadr w całości.
392
-
393
- Regułę trzyma `isFittableBounds(bounds, minSpan?)` z rdzenia (`KPT_MIN_FIT_SPAN` = 0,001°,
394
- czyli ~100 m) — jest w publicznym API, ma testy i obowiązuje każdy adapter, nie tylko Nominatim.
395
- Próg wystarczy przekroczyć w **jednej** osi: długa, wąska ulica ma jeden wymiar mikroskopijny
396
- i dalej zasługuje na `fitBounds`.
397
-
398
- **Zawężanie geograficzne.** Zapytanie dostaje `viewbox` z `[bounds]`, a bez nich z presetu
399
- `[country]` — to sam bias, przestawia kolejność wyników i niczego nie odcina. Twarde
400
- `countrycodes` dokłada się dopiero przy `restrictToCountry` (mapa i tak nie pozwoli
401
- przesunąć widoku poza kraj, więc adres z zagranicy byłby nie do pokazania) albo jawnie
402
- przez `searchOptions.countryCodes`.
403
-
404
- **Tymczasowa pinezka** (`[searchMarker]`, domyślnie włączona) idzie tą samą drogą co reszta
405
- znaczników — przez `engine.setMarkers()` — więc przesuwa się z mapą klatka po klatce.
406
- Nie należy do `entries()`, więc kliknięcie w nią nic nie otwiera. Znika po wyczyszczeniu
407
- pola krzyżykiem i po kliknięciu w mapę.
408
-
409
- Polityka Nominatim jest tu **ostrzejsza niż przy `reverse()`**, bo podpowiadanie w trakcie
410
- pisania z natury generuje serie zapytań. Adapter broni się dwoma mechanizmami: pole czeka
411
- `debounce` (300 ms) od ostatniego znaku i nie pyta poniżej `minLength` (3 znaki), a sam
412
- adapter trzyma **jedną wspólną kolejkę z `reverse()`** — jedno zapytanie na sekundę na całego
413
- klienta, nie na metodę. Zapytanie porzucone (kolejny znak, zamknięcie pola) jest przerywane
414
- `AbortController`-em. Do produkcji i tak podstaw własną instancję albo dostawcę komercyjnego.
415
-
416
- Kolejka liczy odstęp **od zegara**, a nie stałym opóźnieniem przed każdym zapytaniem: pierwsze
417
- po chwili bezczynności leci od razu, a kolejne czeka tylko tyle, ile brakuje do sekundy.
418
- Górna granica jednego zapytania na sekundę zostaje nienaruszona, ale pierwsza podpowiedź
419
- nie kosztuje już sekundy w plecy.
382
+ **After a suggestion is picked the map moves the view itself.** A result with `bounds` (a city, a region,
383
+ a province) gets `fitBounds`, and a result without them — `setView` at `[searchZoom]`. The
384
+ `searchSelect` event fires after the movement and is there for the application's own purposes (moving
385
+ `[distanceFrom]`, querying for nearby points), not for navigation.
386
+
387
+ The Nominatim adapter **deliberately rejects an overly tight box** and returns the point alone in that case.
388
+ A single address has a bounding box on the order of 0.0001° there, so `fitBounds` would jump to
389
+ the top zoom limit on it and every address would land at a different zoom, depending on how
390
+ the provider traced the building — with the threshold, addresses land at a repeatable `[searchZoom]`,
391
+ while cities still fit into the frame whole.
392
+
393
+ The rule is held by `isFittableBounds(bounds, minSpan?)` from the core (`KPT_MIN_FIT_SPAN` = 0.001°,
394
+ that is ~100 m) — it is in the public API, it has tests and it applies to every adapter, not just Nominatim.
395
+ The threshold only has to be exceeded on **one** axis: a long, narrow street has one microscopic dimension
396
+ and still deserves `fitBounds`.
397
+
398
+ **Geographic narrowing.** The query gets a `viewbox` from `[bounds]`, or from the `[country]`
399
+ preset without them — that is bias only, it reorders the results and cuts nothing off. A hard
400
+ `countrycodes` is only added with `restrictToCountry` (the map will not let the view move
401
+ outside the country anyway, so a foreign address would be impossible to show) or explicitly
402
+ through `searchOptions.countryCodes`.
403
+
404
+ **The temporary pin** (`[searchMarker]`, on by default) travels the same path as the other
405
+ markers — through `engine.setMarkers()` — so it moves with the map frame by frame.
406
+ It does not belong to `entries()`, so clicking it opens nothing. It disappears once the field
407
+ is cleared with the × and after a click on the map.
408
+
409
+ The Nominatim policy is **stricter here than for `reverse()`**, because as-you-type suggestions
410
+ generate series of requests by their nature. The adapter defends itself two ways: the field waits
411
+ a `debounce` (300 ms) after the last character and does not query below `minLength` (3 characters), while the
412
+ adapter itself keeps **one queue shared with `reverse()`** — one request per second for the whole
413
+ client, not per method. An abandoned request (another character, the field closing) is aborted with
414
+ an `AbortController`. For production, put your own instance or a commercial provider behind it anyway.
415
+
416
+ The queue measures the interval **against the clock** rather than adding a fixed delay before each request: the first one
417
+ after a quiet moment goes out immediately, and the next waits only for what is left of the second.
418
+ The upper bound of one request per second stays intact, but the first suggestion
419
+ no longer costs a second up front.
420
420
 
421
421
  ### KptMapSearch (kpt-map-search)
422
422
 
423
- Samo pole jest osobnym komponentem — jak legenda, żeby dało się je postawić poza mapą
424
- (pasek narzędzi aplikacji, panel boczny):
423
+ The field itself is a separate component — like the legend, so that it can stand outside the map
424
+ (an application toolbar, a side panel):
425
425
 
426
426
  <kpt-map-search [viewbox]="bounds" (selected)="goTo($event)" (cleared)="clearPin()" />
427
427
 
428
- Wejścia: `placeholder`, `minLength` (3), `debounce` (300), `limit` (5), `countryCodes`,
429
- `viewbox`, `disabled`, `geocoder` (port tego pola; wygrywa nad DI — taką drogą podaje go
430
- `kpt-map`). Wyjścia: `selected: KptMapSearchResult`, `cleared: void`.
431
- Bez portu z `search()` pole renderuje się **wyłączone**, zamiast udawać, że coś znajdzie.
428
+ Inputs: `placeholder`, `minLength` (3), `debounce` (300), `limit` (5), `countryCodes`,
429
+ `viewbox`, `disabled`, `geocoder` (this field's port; it wins over DI — that is how
430
+ `kpt-map` passes it in). Outputs: `selected: KptMapSearchResult`, `cleared: void`.
431
+ Without a port that has `search()` the field renders **disabled**, rather than pretending it will find anything.
432
432
 
433
- **Dlaczego nie `kpt-autocomplete`** — pytanie wraca, więc na piśmie: tamten filtruje opcje
434
- po stronie klienta (`includes(q)`), więc wynik geokodera tolerancyjny na literówki albo inaczej
435
- brzmiący wypadałby z listy; jego wartością jest sam `string`, więc nie ma czym przewieźć
436
- współrzędnych; i stoi na CDK Overlay, co dołożyłoby `@angular/cdk/overlay-prebuilt.css`
437
- do wymagań mapy, której poza tym nie potrzebuje. Panel podpowiedzi jest więc rysowany wprost
438
- w warstwie Angulara nad mapą, tak samo jak popup i menu prawego przycisku.
433
+ **Why not `kpt-autocomplete`** — the question keeps coming back, so in writing: that one filters options
434
+ client-side (`includes(q)`), so a geocoder result that tolerates typos or is phrased differently
435
+ would drop off the list; its value is a plain `string`, so there is nothing to carry
436
+ coordinates in; and it stands on CDK Overlay, which would add `@angular/cdk/overlay-prebuilt.css`
437
+ to the map's requirements when it otherwise needs none. The suggestion panel is therefore drawn directly
438
+ in the Angular layer over the map, just like the popup and the right-click menu.
439
439
 
440
- Debounce stoi na `setTimeout` w efekcie z `onCleanup`, a nie na operatorach rxjs: porty tego
441
- pakietu używają wyłącznie `Observable`/`of`, a rxjs nie jest zadeklarowany jako
442
- `peerDependency` biblioteki (wchodzi tranzytywnie przez `@angular/core`).
440
+ The debounce stands on a `setTimeout` inside an effect with `onCleanup`, not on rxjs operators: this package's
441
+ ports use `Observable`/`of` exclusively, and rxjs is not declared as the library's
442
+ `peerDependency` (it arrives transitively through `@angular/core`).
443
443
 
444
- Wpisany tekst i fraza do odpytania to **dwa osobne sygnały**. Wybór podpowiedzi wpisuje jej
445
- etykietę do pola, ale nie rusza frazy — inaczej wybór natychmiast odpalałby kolejne zapytanie
446
- o coś, co właśnie znaleźliśmy.
444
+ The typed text and the phrase to query are **two separate signals**. Picking a suggestion writes its
445
+ label into the field but does not touch the phrase — otherwise a pick would immediately fire another query
446
+ for what we just found.
447
447
 
448
- ## Legenda
448
+ ## Legend
449
449
 
450
- `kpt-map-legend` — osobny komponent, żeby dało się go umieścić poza mapą. Pozycje są
451
- przyciskami z `aria-pressed`; kliknięcie chowa kategorię (znika też z grup, liczniki maleją).
450
+ `kpt-map-legend` — a separate component, so that it can be placed outside the map. The entries are
451
+ buttons with `aria-pressed`; a click hides the category (it also disappears from the clusters, and the counters drop).
452
452
 
453
453
  <kpt-map [legend]="false" [(hiddenCategories)]="hidden" … />
454
454
  <kpt-map-legend [categories]="categories" [(hidden)]="hidden" orientation="vertical" />
455
455
 
456
- Wejścia: `categories` (wymagane), `hidden` (model), `counts`, `orientation`, `interactive`.
456
+ Inputs: `categories` (required), `hidden` (a model), `counts`, `orientation`, `interactive`.
457
457
 
458
- ## Geolokalizacja
458
+ ## Geolocation
459
459
 
460
- `[showUserLocation]` włącza znacznik własnego położenia (ikona `user`) z kręgiem dokładności
461
- i kontrolkę „wyśrodkuj na mnie". Pozycja idzie przez port DI:
460
+ `[showUserLocation]` enables the marker for the user's own position (the `user` icon) with an accuracy circle
461
+ and a "center on me" control. The position comes through a DI port:
462
462
 
463
- KPT_MAP_GEOLOCATION → KptBrowserGeolocation (domyślnie)
464
- provideKptMapGeolocationStub({ lat: 52.23, lng: 21.01 }) // Storybook, testy
463
+ KPT_MAP_GEOLOCATION → KptBrowserGeolocation (by default)
464
+ provideKptMapGeolocationStub({ lat: 52.23, lng: 21.01 }) // Storybook, tests
465
465
 
466
- Geolocation API działa **wyłącznie w bezpiecznym kontekście** (HTTPS albo `localhost`).
467
- Na `http://` z adresem IP przeglądarka odmówi — to nie jest błąd komponentu. Odmowa zgody
468
- wyłącza kontrolkę i emituje `userLocationError`; komponent nie pokazuje własnego alertu,
469
- decyzję o komunikacie zostawia aplikacji. Na SSR port zwraca `null`.
466
+ The Geolocation API works **only in a secure context** (HTTPS or `localhost`).
467
+ On `http://` with an IP address the browser refuses — that is not a bug in the component. A denied permission
468
+ disables the control and emits `userLocationError`; the component shows no alert of its own, leaving
469
+ the decision about a message to the application. Under SSR the port returns `null`.
470
470
 
471
- ## Silniki
471
+ ## Engines
472
472
 
473
- Silnik dostarcza fabryka pod tokenem `KPT_MAP_ENGINE`, a komponent rozmawia z nią przez
474
- interfejs `KptMapEngine` (zero typów Leafleta w sygnaturach). Podział renderowania:
473
+ The engine is supplied by a factory under the `KPT_MAP_ENGINE` token, and the component talks to it through
474
+ the `KptMapEngine` interface (zero Leaflet types in the signatures). The split of rendering:
475
475
 
476
- - kafle, panning, zoom, **znaczniki i grupy** → silnik (`L.divIcon` z HTML-em z rdzenia),
477
- - **popup, kontrolki, menu, legenda, dialog** → warstwa Angulara nad kontenerem mapy.
476
+ - tiles, panning, zoom, **markers and clusters** → the engine (`L.divIcon` with HTML from the core),
477
+ - **the popup, the controls, the menu, the legend, the dialog** → an Angular layer over the map container.
478
478
 
479
- Znaczniki idą przez silnik, bo muszą przesuwać się z mapą klatka po klatce (Leaflet robi to
480
- jednym `transform` na panie). Popup jest jeden na raz, więc przeliczanie jego pozycji
481
- z `engine.project()` przy każdym ruchu jest tanie — a daje pełnego Angulara w treści
482
- i jedną implementację dla obu silników.
479
+ Markers go through the engine because they have to move with the map frame by frame (Leaflet does that
480
+ with a single `transform` on the pane). There is one popup at a time, so recomputing its position
481
+ with `engine.project()` on every movement is cheap — and it buys full Angular in the content
482
+ and one implementation for both engines.
483
483
 
484
- ### Dwa silniki
484
+ ### Two engines
485
485
 
486
- provideKptLeafletMap() // domyślny, bez klucza i konta
487
- provideKptGoogleMap({ apiKey, mapId }) // Google Maps JavaScript API
486
+ provideKptLeafletMap() // the default, no key and no account
487
+ provideKptGoogleMap({ apiKey, mapId }) // the Google Maps JavaScript API
488
488
 
489
- Leaflet wymaga paczki `leaflet` (opcjonalnej) i `leaflet/dist/leaflet.css`. Google nie wymaga
490
- **żadnej paczki npm** — API dogrywa się skryptem przy pierwszej mapie — ale wymaga klucza,
491
- konta rozliczeniowego z kartą i `mapId` z konsoli (*Map management → Create Map ID*).
492
- Bez `mapId` nie działa `AdvancedMarkerElement`, czyli nie będzie znaczników.
489
+ Leaflet needs the (optional) `leaflet` package and `leaflet/dist/leaflet.css`. Google needs
490
+ **no npm package at all** — the API is pulled in by a script with the first map — but it does need a key,
491
+ a billing account with a card and a `mapId` from the console (*Map management → Create Map ID*).
492
+ Without `mapId` `AdvancedMarkerElement` does not work, which means there will be no markers.
493
493
 
494
494
  | | `leaflet` | `google` |
495
495
  | ---------------------- | ----------------------------- | ----------------------------------------- |
496
- | `[tileUrl]`/`[attribution]` | działa, wymiana w locie | **ignorowane** — kafle daje Google |
497
- | atrybucja | kontrolka Leafleta (ODbL) | logo i linki ToS wbudowane, **nie zasłaniać** |
498
- | klucz / konto | niepotrzebne | klucz API + billing z kartą |
499
- | koszt | kafle OSM (bez SLA) | SKU Dynamic Maps: 10 tys. wczytań/mies. za darmo |
500
- | geokoder Google | **niedozwolony** | dozwolony |
501
- | zoom | całkowity, max 19 | ułamkowy przy gestach, ponad 21 |
496
+ | `[tileUrl]`/`[attribution]` | work, swappable on the fly | **ignored** — Google supplies the tiles |
497
+ | attribution | Leaflet's control (ODbL) | the logo and ToS links are built in, **do not cover them** |
498
+ | key / account | not needed | an API key + billing with a card |
499
+ | cost | OSM tiles (no SLA) | the Dynamic Maps SKU: 10k loads/month free |
500
+ | the Google geocoder | **not allowed** | allowed |
501
+ | zoom | integer, max 19 | fractional on gestures, above 21 |
502
502
 
503
- **Ostatni wiersz jest powodem, dla którego ten silnik istnieje.** Google Maps Platform Service
504
- Specific Terms §6.2 (Geocoding API) i §14.2 (Places API) zabraniają używać ich wyników
505
- „in conjunction with a non-Google map". Adapter Google pod `KPT_MAP_GEOCODER` jest więc zgodny
506
- z umową **wyłącznie** przy silniku `google` — z domyślnym Leafletem nie. Sama biblioteka
507
- takiego adaptera nie dostarcza; to decyzja i kod aplikacji.
503
+ **The last row is the reason this engine exists.** The Google Maps Platform Service
504
+ Specific Terms §6.2 (Geocoding API) and §14.2 (Places API) forbid using their results
505
+ "in conjunction with a non-Google map". A Google adapter under `KPT_MAP_GEOCODER` is therefore compliant
506
+ **only** with the `google` engine — not with the default Leaflet. The library itself
507
+ ships no such adapter; that is the application's decision and code.
508
508
 
509
- Dlatego geokoder jest per mapa, a nie per aplikacja: wejście `[geocoder]` (niżej) pozwala
510
- postawić obok siebie mapę Leafleta z geokoderem OSM i mapę Google z geokoderem Google.
511
- Globalny provider nie umiałby tego rozstrzygnąć — musiałby złamać regułę po jednej
512
- ze stron.
509
+ That is why the geocoder is per map rather than per application: the `[geocoder]` input (below) lets you
510
+ put a Leaflet map with an OSM geocoder next to a Google map with a Google geocoder.
511
+ A global provider could not settle that — it would have to break the rule on one
512
+ of the two sides.
513
513
 
514
- ### `[engine]` — dwie mapy, dwa silniki, jedna strona
514
+ ### `[engine]` — two maps, two engines, one page
515
515
 
516
- Wejście przyjmuje fabrykę i wygrywa nad `KPT_MAP_ENGINE` z DI. Bez niego nic się nie zmienia.
516
+ The input takes a factory and wins over `KPT_MAP_ENGINE` from DI. Without it nothing changes.
517
517
 
518
518
  readonly google: KptMapEngineFactory = () => createGoogleEngine({ apiKey, mapId });
519
519
 
520
520
  <kpt-map [engine]="google" [locations]="places()" [categories]="cats" />
521
521
 
522
- Czytane raz, przy inicjalizacji — podmiana po starcie nie przeładowuje mapy. Brak silnika
523
- w obu źródłach daje czytelny błąd z instrukcją, a nie `NullInjectorError`.
522
+ It is read once, at initialization — swapping it after startup does not reload the map. No engine
523
+ in either source produces a readable error with instructions, not a `NullInjectorError`.
524
524
 
525
- ### `[geocoder]` — port idzie za silnikiem
525
+ ### `[geocoder]` — the port follows the engine
526
526
 
527
- Symetryczne do `[engine]` i z tego samego powodu: geokoder Google wolno pokazać tylko
528
- z mapą Google, a to jest własność **mapy**, nie aplikacji. Wejście wygrywa nad
529
- `KPT_MAP_GEOCODER` z DI i trafia zarówno do wyszukiwarki, jak i do formularza dodawania.
527
+ Symmetrical to `[engine]` and for the same reason: a Google geocoder may only be shown
528
+ with a Google map, and that is a property of the **map**, not of the application. The input wins over
529
+ `KPT_MAP_GEOCODER` from DI and reaches both the search and the create form.
530
530
 
531
- <kpt-map [engine]="leaflet" search … /> <!-- port z DI (np. Nominatim) -->
531
+ <kpt-map [engine]="leaflet" search … /> <!-- the port from DI (Nominatim, say) -->
532
532
  <kpt-map [engine]="google" [geocoder]="googleGeocoder" search … />
533
533
 
534
- W odróżnieniu od `[engine]` czytane jest **reaktywnie**: podmiana portu zeruje listę
535
- podpowiedzi i od następnej frazy pyta nowego dostawcę. `null` w obu źródłach znaczy tyle,
536
- co dziś brak portu: pole wyszukiwarki się nie renderuje, a formularz nie podpowiada adresu.
534
+ Unlike `[engine]` it is read **reactively**: swapping the port clears the suggestion list
535
+ and queries the new provider from the next phrase on. `null` in both sources means what
536
+ having no port means today: the search field does not render, and the form suggests no address.
537
537
 
538
- To samo wejście ma `kpt-map-search` i `kpt-map-create-dialog`, gdy stoją poza mapą.
538
+ `kpt-map-search` and `kpt-map-create-dialog` have the same input when they stand outside a map.
539
539
 
540
- **Pisząc adapter Google, idź przez SDK, nie przez REST.** Web service
541
- `maps.googleapis.com/maps/api/geocode/json` odrzuca klucze ograniczone po domenie:
540
+ **When writing a Google adapter, go through the SDK, not through REST.** The
541
+ `maps.googleapis.com/maps/api/geocode/json` web service rejects keys restricted by domain:
542
542
 
543
543
  { "status": "REQUEST_DENIED",
544
544
  "error_message": "API keys with referer restrictions cannot be used with this API." }
545
545
 
546
- a to jedyna restrykcja, jaka ma sens dla klucza leżącego jawnie w bundlu przeglądarki. CORS na
547
- tym endpoincie działa, więc nie ma nawet błędu sieci — przychodzi `200` z `REQUEST_DENIED`
548
- w treści, łatwe do przeoczenia. `google.maps.Geocoder` z załadowanego SDK autoryzuje się tak samo jak sama
549
- mapa, więc działa z tym samym kluczem — to ten sam SKU, tylko inna droga. Dostęp do API
550
- daje wyeksportowany loader (ten sam, którego używa silnik):
546
+ and that is the only restriction that makes sense for a key sitting in plain sight in a browser bundle. CORS on
547
+ that endpoint works, so there is not even a network error — a `200` arrives with `REQUEST_DENIED`
548
+ in the body, easy to miss. `google.maps.Geocoder` from the loaded SDK authorizes the same way the map itself
549
+ does, so it works with the same key — the same SKU, just a different route. Access to the API
550
+ comes from the exported loader (the same one the engine uses):
551
551
 
552
552
  const maps = await loadGoogleMapsApi({ apiKey, language: 'pl' });
553
553
  const { Geocoder } = await maps.importLibrary('geocoding');
554
554
  const { results } = await new Geocoder().geocode({ address: 'Marszałkowska 100, Warszawa' });
555
555
 
556
- Uwaga na kształt odpowiedzi: SDK oddaje `location` i `viewport` jako obiekty z metodami
557
- (`lat()`, `getNorthEast()`), a nie jako pola jak REST. Zapytania SDK **nie da się przerwać**,
558
- więc `signal` z opcji portu zostaje bez pokrycia — odsubskrybowanie tylko przestaje słuchać.
559
- Przykład adaptera od początku do końca: `apps/playground/src/pg-map.google-geocoder.ts`.
556
+ Mind the shape of the response: the SDK returns `location` and `viewport` as objects with methods
557
+ (`lat()`, `getNorthEast()`), not as fields the way REST does. An SDK request **cannot be aborted**,
558
+ so the `signal` from the port's options has nothing behind it — unsubscribing merely stops listening.
559
+ An adapter from start to finish: `apps/playground/src/pg-map.google-geocoder.ts`.
560
560
 
561
- Silnik trafia na host jako `[data-engine]` (`leaflet` | `google`) — po tym atrybucie stylują
562
- się nieliczne różnice, np. krąg dokładności (w Leaflecie kółko SVG, w Google element z tłem,
563
- bo `google.maps.Circle` przyjmuje kolory w opcjach, a tu obowiązują tokeny).
561
+ The engine lands on the host as `[data-engine]` (`leaflet` | `google`) — that attribute is what the few
562
+ differences are styled by, such as the accuracy circle (an SVG circle in Leaflet, an element with a background in
563
+ Google, because `google.maps.Circle` takes colors in its options while tokens are the rule here).
564
564
 
565
- Adapter Google liczy `project()`/`unproject()` **rdzeniem** (`core/geo.ts`), a nie
566
- `map.getProjection()`: ten bywa `null` do pierwszego `idle` i tak czy owak wymaga przeliczenia
567
- na piksele kontenera. Dzięki temu popup ląduje piksel w piksel tam samo co przy Leaflecie,
568
- zgadza się z grupowaniem i radzi sobie z zoomem ułamkowym.
565
+ The Google adapter computes `project()`/`unproject()` **in the core** (`core/geo.ts`) rather than with
566
+ `map.getProjection()`: that one can be `null` until the first `idle` and needs converting
567
+ into container pixels anyway. As a result the popup lands pixel for pixel where it does with Leaflet,
568
+ agrees with the clustering and copes with fractional zoom.
569
569
 
570
- ## Rdzeń (pure TS)
570
+ ## Core (pure TS)
571
571
 
572
- `@konce-pt/angular/map` eksportuje rdzeń obliczeń — bez Angulara i bez silnika, przenośny
573
- do portu React: `clusterLocations`, `dominantCategory`, `countByCategory`, `isSplittable`,
572
+ `@konce-pt/angular/map` exports the computation core — with no Angular and no engine, portable
573
+ to the React port: `clusterLocations`, `dominantCategory`, `countByCategory`, `isSplittable`,
574
574
  `project`/`unproject`/`boundsOf`/`haversine`/`metersPerPixel`/`zoomForBounds`/`isFittableBounds`,
575
575
  `KPT_MAP_COUNTRIES`/`countryPreset`/`resolveCountryView`,
576
576
  `renderMarkerHtml`/`renderClusterHtml`/`colorVarOf`/`contrastVarOf`/`foregroundVarOf`,
577
- `formatDistance`/`formatLatLng` oraz godziny otwarcia: `parseTime`, `formatTime`, `isoWeekday`,
577
+ `formatDistance`/`formatLatLng` and the opening hours: `parseTime`, `formatTime`, `isoWeekday`,
578
578
  `isOpenAt`, `isAlwaysOpen`, `nextChange`, `weekdayName`, `groupWeek`, `emptyWeek`.
579
579
 
580
- Model godzin: `KptMapOpeningHours` = tablica `{ day: 1–7 (ISO), closed?, ranges: [{ from, to }] }`
581
- w `HH:MM` czasu lokalnego miejsca. `to` mniejsze albo równe `from` znaczy „przez północ";
582
- `24:00` to prawidłowy koniec doby i przez północ **nie** przechodzi. Brakujący dzień = zamknięte.
583
- `isOpenAt()` sprawdza dwa dni — bieżący i poprzedni — bo przedział `22:00–01:00` obejmuje
584
- wczesne godziny dnia następnego. `groupWeek()` skleja sąsiadujące dni o identycznym rozkładzie
585
- (`pon.–pt.`), ale **nie** zawija tygodnia przez granicę niedziela–poniedziałek: tydzień czyta się
586
- liniowo i zawijanie byłoby mylące.
580
+ The hours model: `KptMapOpeningHours` = an array of `{ day: 1–7 (ISO), closed?, ranges: [{ from, to }] }`
581
+ in `HH:MM` of the place's local time. A `to` lower than or equal to `from` means "past midnight";
582
+ `24:00` is a valid end of day and does **not** cross midnight. A missing day = closed.
583
+ `isOpenAt()` checks two days — the current one and the previous one — because a `22:00–01:00` range covers
584
+ the early hours of the next day. `groupWeek()` joins adjacent days with an identical schedule
585
+ (`Mon–Fri`), but it does **not** wrap the week across the Sunday–Monday boundary: a week reads
586
+ linearly and wrapping would be confusing.
587
587
 
588
- Testy: `packages/angular/map/src/core/*.test.ts` (`node --test`).
588
+ Tests: `packages/angular/map/src/core/*.test.ts` (`node --test`).
589
589
 
590
- ## Grupowanie — jak działa i czego nie robi
590
+ ## Clustering — how it works and what it does not do
591
591
 
592
- Siatka w przestrzeni pikseli: punkty rzutowane na piksele świata dla aktualnego zoomu
593
- i przypisane do komórek `gridSize` px. Algorytm jest O(n) i deterministyczny — ten sam
594
- zestaw danych zawsze daje te same grupy o tych samych identyfikatorach.
592
+ A grid in pixel space: points are projected onto world pixels for the current zoom
593
+ and assigned to `gridSize` px cells. The algorithm is O(n) and deterministic — the same
594
+ data set always yields the same clusters with the same ids.
595
595
 
596
- Środek grupy uśredniamy **w pikselach, nie w stopniach**: Mercator rozciąga stopnie ku
597
- biegunom, więc średnia w stopniach wypada obok wizualnego środka kropek na ekranie.
596
+ The cluster center is averaged **in pixels, not in degrees**: Mercator stretches degrees toward the
597
+ poles, so an average in degrees lands beside the visual center of the dots on screen.
598
598
 
599
- Ograniczenia (świadome):
600
- - grupa może „przeskoczyć" przy przekroczeniu granicy komórki podczas zoomowania — to cena
601
- za brak preprocessingu, jaki wymaga clustering hierarchiczny;
602
- - **nie ma spiderfy.** Punkty o identycznej współrzędnej zostaną razem na każdym zoomie,
603
- więc kliknięcie takiej grupy otwiera popup z listą pozycji zamiast zoomować w nieskończoność
604
- (`isSplittable()` rozpoznaje ten przypadek);
605
- - powyżej `clusterMaxZoom` (domyślnie 16) grupowanie jest wyłączone.
599
+ The limitations (deliberate):
600
+ - a cluster can "jump" when a cell boundary is crossed while zooming — that is the price
601
+ for skipping the preprocessing hierarchical clustering requires;
602
+ - **there is no spiderfy.** Points with identical coordinates stay together at every zoom,
603
+ so clicking such a cluster opens a popup listing the entries instead of zooming forever
604
+ (`isSplittable()` recognizes that case);
605
+ - above `clusterMaxZoom` (16 by default) clustering is switched off.
606
606
 
607
- ## Kafle — polityka OSM
607
+ ## Tiles — the OSM policy
608
608
 
609
- Domyślne `https://tile.openstreetmap.org/{z}/{x}/{y}.png` to usługa **best-effort bez SLA**.
610
- Fundacja OSM wymaga widocznej atrybucji i sensownego `User-Agent`/`Referer`, zabrania
611
- pre-fetchowania i zastrzega sobie blokowanie ruchu bez uprzedzenia — w szczególności
612
- komercyjnego. Domyślne źródło jest po to, żeby demo działało od razu.
609
+ The default `https://tile.openstreetmap.org/{z}/{x}/{y}.png` is a **best-effort service with no SLA**.
610
+ The OSM Foundation requires visible attribution and a sensible `User-Agent`/`Referer`, forbids
611
+ pre-fetching and reserves the right to block traffic without notice — commercial traffic
612
+ in particular. The default source is there so that the demo works right away.
613
613
 
614
- **Do produkcji podstaw własny lub komercyjny serwer kafli** przez `[tileUrl]` + `[attribution]`.
615
- Atrybucji nie usuwaj — to wymóg licencyjny, nie ozdoba.
614
+ **For production, put your own or a commercial tile server behind it** through `[tileUrl]` + `[attribution]`.
615
+ Do not remove the attribution — it is a license requirement, not decoration.
616
616
 
617
- ## Tokeny
617
+ ## Tokens
618
618
 
619
619
  `--kpt-map-bg`, `--kpt-map-border`, `--kpt-map-radius`, `--kpt-map-popup-bg`,
620
620
  `--kpt-map-popup-shadow`, `--kpt-map-popup-width`, `--kpt-map-popup-width-card`,
621
621
  `--kpt-map-popup-max-height`, `--kpt-map-search-width`, `--kpt-map-search-panel-max-height`,
622
622
  `--kpt-map-poi-media-radius`, `--kpt-map-poi-row-hover`,
623
623
  `--kpt-map-cluster-bg`, `--kpt-map-cluster-fg`, `--kpt-map-marker-size`,
624
- `--kpt-map-height` (z `[height]`).
624
+ `--kpt-map-height` (from `[height]`).
625
625
 
626
- Kolory znaczników i grup biorą się z ról kategorii: `--kpt-color-{success,warning,danger,
627
- info,primary,muted}` i ich warianty `-contrast`.
626
+ Marker and cluster colors come from the category roles: `--kpt-color-{success,warning,danger,
627
+ info,primary,muted}` and their `-contrast` variants.
628
628
 
629
- ## Kontekst układania Leafleta — nie ruszać
629
+ ## Leaflet's stacking context — do not touch
630
630
 
631
- `.kpt-map .leaflet-container` dostaje `z-index: 0`, żeby zamknąć skalę z-index Leafleta
632
- (panes 200–700, kontrolki 800) we własnym kontekście układania. Bez tego panes konkurują
633
- bezpośrednio z warstwą Angulara rozpiętą nad mapą — a popup (`z-index: 3`), kontrolki zoomu
634
- (2) i menu prawego przycisku (4) przegrywają i są **malowane pod kaflami**.
631
+ `.kpt-map .leaflet-container` gets `z-index: 0`, to close Leaflet's z-index scale
632
+ (panes 200–700, controls 800) inside its own stacking context. Without that the panes compete
633
+ directly with the Angular layer stretched over the map — and the popup (`z-index: 3`), the zoom controls
634
+ (2) and the right-click menu (4) lose and are **painted under the tiles**.
635
635
 
636
- Usterka jest podstępna, bo hit-test jej nie pokazuje: panes i kafle mają `pointer-events: none`,
637
- więc `document.elementFromPoint` zwraca popup jako element na wierzchu, mimo że użytkownik go
638
- nie widzi. Jedyny wiarygodny test to zrzut ekranu.
636
+ The bug is insidious, because a hit test does not show it: the panes and tiles have `pointer-events: none`,
637
+ so `document.elementFromPoint` returns the popup as the topmost element even though the user
638
+ cannot see it. The only reliable test is a screenshot.
639
639
 
640
- Skala warstwy Angulara nad mapą: kontrolki zoomu `2`, popup `3`, menu prawego przycisku `4`,
641
- wyszukiwarka `5` (rozwinięty panel podpowiedzi ma zakrywać wszystko powyższe).
640
+ The scale of the Angular layer over the map: zoom controls `2`, the popup `3`, the right-click menu `4`,
641
+ the search `5` (an expanded suggestion panel has to cover everything above).
642
642
 
643
- ## Mapa nie stylizuje hostów komponentów potomnych — nie „upraszczać"
643
+ ## The map does not style child components' hosts — do not "simplify"
644
644
 
645
- Wyszukiwarkę pozycjonuje **opakowanie** `<div class="kpt-map__search">`, a nie klasa
646
- postawiona wprost na `<kpt-map-search>`. Skrócenie tego do jednego elementu wygląda
647
- niewinnie i **chowa pole całkowicie**.
645
+ The search is positioned by a **wrapper**, `<div class="kpt-map__search">`, and not by a class
646
+ placed directly on `<kpt-map-search>`. Shortening that to a single element looks
647
+ innocent and **hides the field completely**.
648
648
 
649
- Powód: host wyszukiwarki ma własną regułę `.kpt-map-search { position: relative }` (blok
650
- zawierający dla panelu podpowiedzi, potrzebny też poza mapą). Obie reguły trafiają wtedy na ten
651
- sam element z identyczną specyficznością (0,1,0) i w tej samej warstwie `kpt.components`, więc
652
- rozstrzyga kolejność wstrzyknięcia — a Angular wstrzykuje style dziecka **po** stylach rodzica.
653
- `position: relative` wygrywa z `absolute`, pole wypada do normalnego przepływu za
654
- `.kpt-map__canvas` (`height: 100%`) i zostaje przycięte przez `overflow: hidden` widoku.
649
+ The reason: the search host has its own `.kpt-map-search { position: relative }` rule (the containing
650
+ block for the suggestion panel, needed outside the map too). Both rules would then land on the
651
+ same element with identical specificity (0,1,0) and in the same `kpt.components` layer, so
652
+ injection order decides — and Angular injects a child's styles **after** the parent's.
653
+ `position: relative` beats `absolute`, the field drops into normal flow behind
654
+ `.kpt-map__canvas` (`height: 100%`) and is clipped by the view's `overflow: hidden`.
655
655
 
656
- Usterka jest podstępna z tego samego powodu co ta z z-indexem: w DOM element jest, ma poprawne
657
- atrybuty i przechodzi każdą kontrolę „czy się wyrenderował". Widać ją wyłącznie na zrzucie ekranu.
656
+ The bug is insidious for the same reason as the z-index one: the element is in the DOM, has the right
657
+ attributes and passes every "did it render" check. It only shows up in a screenshot.
658
658
 
659
- `class="kpt-map__legend"` na `<kpt-map-legend>` jest bezpieczne, bo `map.component.scss`
660
- **nie ma dla tej klasy żadnej reguły** — to czysty hak do zaczepienia z zewnątrz. Reguła
661
- dopisana tam kiedykolwiek w przyszłości wpadnie w tę samą pułapkę.
659
+ `class="kpt-map__legend"` on `<kpt-map-legend>` is safe, because `map.component.scss`
660
+ **has no rule for that class** — it is a pure hook for attaching things from outside. A rule
661
+ added there at any point in the future would fall into the same trap.
662
662
 
663
- ## Odstępstwo od `@layer` — nie „naprawiać"
663
+ ## A deviation from `@layer` — do not "fix" this
664
664
 
665
- `map.component.scss` ma dwie części: style własne w `@layer kpt.components` oraz sekcję
666
- nadpisań `.leaflet-*` **poza warstwą**. To jedyne takie miejsce w bibliotece i jest celowe:
667
- `leaflet.css` dostarcza aplikacja i nie jest warstwowany, a CSS spoza warstw wygrywa
668
- z każdą regułą w `@layer` **niezależnie od specyficzności**. Przeniesienie tych reguł
669
- do warstwy „dla porządku" sprawi, że przestaną działać.
665
+ `map.component.scss` has two parts: its own styles in `@layer kpt.components` and a section
666
+ of `.leaflet-*` overrides **outside the layer**. It is the only such place in the library and it is deliberate:
667
+ `leaflet.css` is supplied by the application and is not layered, and CSS outside layers beats
668
+ every rule inside `@layer` **regardless of specificity**. Moving those rules
669
+ into the layer "for tidiness" will stop them from working.
670
670
 
671
671
  ## A11y
672
672
 
673
- - znacznik: `role="img"` + `aria-label` = `title` lokalizacji,
674
- - grupa: `aria-label` = `map.cluster` z liczbą,
675
- - legenda: `role="group"`, pozycje jako `<button aria-pressed>`,
676
- - popup: `role="dialog"` z `aria-label` = tytuł; zamyka `Esc`, kliknięcie w mapę i przycisk ×
677
- (w `popupLayout="card"` krzyżyk daje karta, żeby nie było dwóch),
678
- - karta miejsca: tytuł jako `role="heading" aria-level="3"`, kontakty jako `<ul>`/`<li>`,
679
- godziny jako disclosure z `aria-expanded` i `aria-controls`, potwierdzenie kopiowania
680
- przez `aria-live="polite"`,
681
- - wyszukiwarka: `role="combobox"` + `aria-expanded`/`aria-controls`/`aria-autocomplete="list"`,
682
- lista jako `<ul role="listbox">` / `<li role="option">`, aktywna pozycja przez
683
- `aria-activedescendant` (panel stoi w DOM obok pola, więc fokus zostaje w `<input>`),
684
- klawiatura ↑/↓/Enter, pierwszy Esc zamyka listę, drugi czyści pole; liczba wyników
685
- i „brak wyników" idą w `aria-live="polite"`,
686
- - kontrolki zoomu i „wyśrodkuj na mnie": `<button>` z `aria-label` ze słownika,
687
- - menu prawego przycisku: `role="menu"` / `role="menuitem"`.
673
+ - a marker: `role="img"` + `aria-label` = the location's `title`,
674
+ - a cluster: `aria-label` = `map.cluster` with the count,
675
+ - the legend: `role="group"`, entries as `<button aria-pressed>`,
676
+ - the popup: `role="dialog"` with `aria-label` = the title; closed by `Esc`, a click on the map and the × button
677
+ (with `popupLayout="card"` the card supplies the close button, so there are not two),
678
+ - the place card: the title as `role="heading" aria-level="3"`, the contacts as `<ul>`/`<li>`,
679
+ the hours as a disclosure with `aria-expanded` and `aria-controls`, copy confirmation
680
+ through `aria-live="polite"`,
681
+ - the search: `role="combobox"` + `aria-expanded`/`aria-controls`/`aria-autocomplete="list"`,
682
+ the list as `<ul role="listbox">` / `<li role="option">`, the active entry through
683
+ `aria-activedescendant` (the panel sits in the DOM next to the field, so focus stays in the `<input>`),
684
+ the ↑/↓/Enter keys, the first Esc closes the list, the second clears the field; the result count
685
+ and "no results" go to `aria-live="polite"`,
686
+ - the zoom controls and "center on me": `<button>` with an `aria-label` from the dictionary,
687
+ - the right-click menu: `role="menu"` / `role="menuitem"`.
688
688
 
689
689
  ## i18n
690
690
 
691
- Klucze `map.*` w `KptMessages` (EN + PL): `addLocation`, `chooseCategory`, `myLocation`,
691
+ The `map.*` keys in `KptMessages` (EN + PL): `addLocation`, `chooseCategory`, `myLocation`,
692
692
  `locateMe`, `locationDenied`, `locationUnavailable`, `zoomIn`, `zoomOut`, `legend`,
693
693
  `showCategory`, `hideCategory`, `cluster`, `clusterItems`, `fieldRequired`, `latitude`,
694
694
  `longitude`, `empty`, `save`, `cancel`, `lookingUpAddress`, `addressNotFound`.
695
695
 
696
- Wyszukiwarka: `map.search.*` — `placeholder`, `label`, `results`, `clear`, `noResults`,
696
+ The search: `map.search.*` — `placeholder`, `label`, `results`, `clear`, `noResults`,
697
697
  `minLength` ({count}), `searching`, `found` ({count}).
698
698
 
699
- Karta miejsca: `map.poi.*` — `distanceAway`, `openNow`, `closedNow`, `opensAt`, `closesAt`,
699
+ The place card: `map.poi.*` — `distanceAway`, `openNow`, `closedNow`, `opensAt`, `closesAt`,
700
700
  `alwaysOpen`, `openingHours`, `closedDay`, `call`, `sendEmail`, `openWebsite`, `coordinates`,
701
701
  `copy`, `copied`, `share`, `addFavourite`, `removeFavourite`, `noPhoto`.
702
702
 
703
- Nowe pola formularza: `map.field.*` — `invalidEmail`, `invalidUrl`, `imageTooLarge`, `pickImage`,
703
+ The newer form fields: `map.field.*` — `invalidEmail`, `invalidUrl`, `imageTooLarge`, `pickImage`,
704
704
  `orPasteUrl`, `removeImage`, `imagePreview`, `hoursClosed`, `hoursFrom`, `hoursTo`,
705
705
  `hoursSameAllWeek`, `hoursAddRange`, `hoursRemoveRange`, `hoursIncomplete`.
706
706
 
707
- Nazw dni tygodnia w słowniku **nie ma** — bierze je `Intl.DateTimeFormat` z locale, więc karta
708
- i edytor godzin działają dla dowolnego języka bez dopisywania tłumaczeń.
707
+ Weekday names are **not** in the dictionary — `Intl.DateTimeFormat` takes them from the locale, so the card
708
+ and the hours editor work for any language without adding translations.
709
709
 
710
- ## Przykład
710
+ ## Example
711
711
 
712
712
  // main.ts
713
713
  bootstrapApplication(App, {
714
714
  providers: [provideKptTablerIcons(), provideKptLeafletMap()],
715
715
  });
716
716
 
717
- // komponent
717
+ // the component
718
718
  readonly locations = signal<KptMapLocation<Site>[]>(SITES);
719
719
  readonly categories: KptMapCategory[] = [
720
- { id: 'opening', label: 'Otwarcie', color: 'success', icon: 'building-store', fields: [ … ] },
721
- { id: 'closure', label: 'Zamknięcie', color: 'danger', icon: 'lock', fields: [ … ] },
720
+ { id: 'opening', label: 'Opening', color: 'success', icon: 'building-store', fields: [ … ] },
721
+ { id: 'closure', label: 'Closure', color: 'danger', icon: 'lock', fields: [ … ] },
722
722
  ];
723
723
 
724
724
  onCreate(event: KptMapCreateEvent): void {
725
725
  this.locations.update((all) => [...all, toLocation(event)]);
726
726
  }
727
727
 
728
- // szablon
728
+ // the template
729
729
  <kpt-map
730
730
  [locations]="locations()"
731
731
  [categories]="categories"
@@ -736,6 +736,6 @@ i edytor godzin działają dla dowolnego języka bez dopisywania tłumaczeń.
736
736
  >
737
737
  <ng-template kptMapPopup category="opening" let-loc let-d="data">
738
738
  <strong>{{ loc.title }}</strong>
739
- <p>Otwarcie: {{ d.openingDate }}</p>
739
+ <p>Opening: {{ d.openingDate }}</p>
740
740
  </ng-template>
741
741
  </kpt-map>