ng-hub-ui-forms 22.33.2 → 22.35.0

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.
package/README.md CHANGED
@@ -51,10 +51,12 @@ npm install ng-hub-ui-forms
51
51
  ```
52
52
 
53
53
  `ng-hub-ui-utils` is a peer dependency (the datepicker opens its panel through its overlay
54
- service, and every field renders its tooltip helper text with `hubTooltip`):
54
+ service, and every field renders its tooltip helper text with `hubTooltip`). `ng-hub-ui-ds`
55
+ carries the design tokens the fields read; it is optional, because every token is read with a
56
+ fallback, but without it the fields fall back to their own defaults instead of your palette:
55
57
 
56
58
  ```bash
57
- npm install ng-hub-ui-utils
59
+ npm install ng-hub-ui-utils ng-hub-ui-ds
58
60
  ```
59
61
 
60
62
  ### 2. Import
@@ -93,7 +95,7 @@ mode — no Bootstrap dependency.
93
95
 
94
96
  ## 🎯 Features
95
97
 
96
- - **Fields** — `hub-input` (text/number/email/password/color/switch/checkbox/counter, with input-group addons & masks, projected in-field affixes, a built-in `clearable` button, the mixed `indeterminate` state on checkboxes and debounced typeahead `search`; the `file` format is **deprecated** → use `hub-file-input`), `hub-otp-input`, `hub-textarea` (+ `hubAutoresize`), `hub-slider` (single / dual thumb, gradient fill), `hub-segmented` (segmented control field — single & multiple selection, horizontal & vertical, with label + validation), `hub-select` (dropdown format, grouping, client-side search via `searchable` **and** server-side async typeahead via a `typeahead` Subject, tag creation with `addTag`, custom templates, `prepend` / `append` group addons and attached icons/buttons via `hubPrepend` / `hubAppend`; the `buttons` / `checkbox` / `radio` formats are **deprecated** → use `hub-segmented`), `hub-datepicker` (single & range at any granularity from a year to a second, time picking, min/max down to the minute, keyboard nav, i18n), `hub-timepicker` (a time of day as `HH:MM`, on the platform's own time control, with `min` / `max` / `step`), `hub-file-input` (drag & drop, clipboard paste, type/size limits, previews, optional upload progress).
98
+ - **Fields** — `hub-input` (text/number/email/password/color/switch/checkbox/counter, the colour format as a hex field or, given a palette, a grid of swatches, with input-group addons & masks, projected in-field affixes, a built-in `clearable` button, the mixed `indeterminate` state on checkboxes and debounced typeahead `search`; the `file` format is **deprecated** → use `hub-file-input`), `hub-otp-input`, `hub-textarea` (+ `hubAutoresize`), `hub-slider` (single / dual thumb, gradient fill), `hub-segmented` (segmented control field — single & multiple selection, horizontal & vertical, with label + validation), `hub-select` (dropdown format, grouping, client-side search via `searchable` **and** server-side async typeahead via a `typeahead` Subject, tag creation with `addTag`, custom templates, `prepend` / `append` group addons and attached icons/buttons via `hubPrepend` / `hubAppend`; the `buttons` / `checkbox` / `radio` formats are **deprecated** → use `hub-segmented`), `hub-datepicker` (single & range at any granularity from a year to a second, time picking, min/max down to the minute, keyboard nav, i18n), `hub-timepicker` (a time of day as `HH:MM`, on the platform's own time control, with `min` / `max` / `step`), `hub-file-input` (drag & drop, clipboard paste, type/size limits, previews as a list, as tiles or inside the field together with the files a record already has, optional upload progress).
97
99
  - **Automatic error display** — bind a field and its control errors render below it; `fieldset[hubFieldset]`, `form[hubForm]` and `hub-legend` surface group- and form-level (cross-field) errors the same way, with zero wiring.
98
100
  - **Containers** — `fieldset[hubFieldset]` (or the `<hub-fieldset>` element) / `form[hubForm]` group fields and show their group errors; `hub-legend` renders an accessible legend.
99
101
  - **Configurable** — `provideHubForms({ … })` sets the invalid-feedback templates, datepicker locale/labels, file-input labels and more, app-wide or per instance.
@@ -131,7 +133,7 @@ section.
131
133
  ## 📦 Installation
132
134
 
133
135
  ```bash
134
- npm install ng-hub-ui-forms ng-hub-ui-utils
136
+ npm install ng-hub-ui-forms ng-hub-ui-utils ng-hub-ui-ds
135
137
  ```
136
138
 
137
139
  ### Peer Dependencies
@@ -142,6 +144,7 @@ npm install ng-hub-ui-forms ng-hub-ui-utils
142
144
  "@angular/core": ">=21.0.0",
143
145
  "@angular/forms": ">=21.0.0",
144
146
  "@angular/platform-browser": ">=21.0.0",
147
+ "ng-hub-ui-ds": ">=22.0.0",
145
148
  "ng-hub-ui-utils": ">=22.12.0"
146
149
  }
147
150
  ```
@@ -192,6 +195,57 @@ string, so asking it to carry markup would drop the markup silently.
192
195
  <hub-input formControlName="darkMode" type="switch" label="Dark mode" />
193
196
  ```
194
197
 
198
+ #### Colour fields
199
+
200
+ `type="color"` is a text field for the hex code, with the colour in a square at its start. The square
201
+ opens the browser's picker. The text takes a colour typed with or without `#`, in three or six digits,
202
+ and the form stores it as lowercase `#rrggbb`, the one notation the native picker reads. Invalid text
203
+ leaves the value alone and goes back to the last valid colour on blur.
204
+
205
+ Give the field a list of colours and it becomes a grid of swatches, one row the height of a field:
206
+
207
+ ```html
208
+ <hub-input formControlName="status" type="color" label="Status colour" [swatches]="palettes.status" />
209
+
210
+ <hub-input
211
+ formControlName="tag"
212
+ type="color"
213
+ label="Tag"
214
+ [swatches]="['#ef4444', { value: '#22c55e', label: 'Done' }]"
215
+ [allowCustomColor]="false"
216
+ />
217
+ ```
218
+
219
+ ```ts
220
+ import { HUB_COLOR_PALETTES } from 'ng-hub-ui-forms';
221
+
222
+ readonly palettes = HUB_COLOR_PALETTES;
223
+ ```
224
+
225
+ - A swatch is any CSS colour that `parseColor` from `ng-hub-ui-utils` reads (hex, `rgb()`, `hsl()`,
226
+ `oklch()`, `oklab()`, a named colour), bare or as `{ value, label }`. The label is what a screen reader
227
+ says, so name the colours that have a name. The control receives the string exactly as written. An
228
+ entry that is not a colour is dropped, with a warning in development builds.
229
+ - The last cell opens the native picker for a colour outside the list. `[allowCustomColor]="false"`
230
+ leaves it out for a closed palette; `customColorLabel` names it.
231
+ - The cells share the row down to `--hub-input-swatch-min-width`, then wrap onto more rows. Once they
232
+ wrap the field drops its box; `--hub-input-swatch-wrapped-border-color` and `-wrapped-bg` bring it back.
233
+ - The grid is a radio group named by the field label, with one Tab stop; the arrows, Home and End move
234
+ the selection.
235
+ - `HUB_COLOR_PALETTES` has five frozen lists of lowercase hex, each swatch named in English: `tailwind`
236
+ (17), `material` (19), `pastel` (17), `neutral` (11) and `status` (5).
237
+
238
+ Which field is drawn:
239
+
240
+ | `swatches` | Application palette (`provideHubForms`) | Result |
241
+ | ---------------- | --------------------------------------- | -------------------------------- |
242
+ | `null` (default) | none (default) | hex field |
243
+ | `null` | a list | grid with the application palette |
244
+ | `[]` | any | hex field |
245
+ | a list | any | grid with the field's list |
246
+
247
+ A list in which no entry is a colour also leaves the hex field.
248
+
195
249
  #### Icon affix & typeahead (search boxes)
196
250
 
197
251
  Project a leading / trailing icon **inside** the field, emit a debounced term on every keystroke, and let the field render its own clear button:
@@ -315,6 +369,38 @@ much room is left to type and so promises typing.
315
369
  <hub-select formControlName="city" label="City" [items]="cities" bindLabel="name" bindValue="id" groupBy="country" />
316
370
  ```
317
371
 
372
+ #### Customization slots
373
+
374
+ Every part of the panel can be re-drawn from a template. The slots carry this library's own name;
375
+ the `ng-*-tmp` attributes of the vendored engine underneath are deprecated and disappear in 23.0.0.
376
+
377
+ ```html
378
+ <hub-select formControlName="assignee" label="Assignee" [items]="people" bindLabel="name">
379
+ <ng-template hubSelectLabel let-item="item">{{ item.emoji }} {{ item.name }}</ng-template>
380
+ <ng-template hubSelectOption let-item="item">
381
+ <strong>{{ item.name }}</strong>
382
+ <small>{{ item.role }}</small>
383
+ </ng-template>
384
+ </hub-select>
385
+ ```
386
+
387
+ | Slot | Draws | Context |
388
+ | --- | --- | --- |
389
+ | `hubSelectOption` | one option in the list | `item`, `item$`, `index`, `searchTerm` |
390
+ | `hubSelectOptgroup` | a group header, with `groupBy` | `item`, `item$`, `index`, `searchTerm` |
391
+ | `hubSelectLabel` | the selected value, single mode | `item`, `label`, `clear` |
392
+ | `hubSelectMultiLabel` | all selected values at once, multiple mode | `items`, `clear` |
393
+ | `hubSelectHeader` | a fixed block above the list | `searchTerm` |
394
+ | `hubSelectFooter` | a fixed block below the list | `searchTerm` |
395
+ | `hubSelectNotFound` | the "no items found" message | `searchTerm` |
396
+ | `hubSelectTypeToSearch` | the "type to search" hint | — |
397
+ | `hubSelectLoadingText` | the "loading…" message | `searchTerm` |
398
+ | `hubSelectLoadingSpinner` | the spinner in the control | — |
399
+ | `hubSelectTag` | the "add \<term\>" row, with `addTag` | `searchTerm` |
400
+ | `hubSelectClearButton` | the clear (×) control | — |
401
+
402
+ Import the directive you use — `HubSelectOptionDirective`, `HubSelectLabelDirective` and so on.
403
+
318
404
  #### Floating label
319
405
 
320
406
  ```html
@@ -562,7 +648,7 @@ Whatever the uploader reports on `done` is kept on the item, so the ids the serv
562
648
  const uploadedIds = fileInput.files().map((item) => (item.response as { id: string }).id);
563
649
  ```
564
650
 
565
- Customize it without forking the template: the `--hub-file-input-*` tokens (every icon is a swappable CSS mask), the `hub-file-input-theme(...)` mixin, and three projection slots.
651
+ Customize it without forking the template: the `--hub-file-input-*` tokens (every icon is a swappable CSS mask), the `hub-file-input-theme(...)` mixin, and three projection slots. `hubFileIcon` applies to `preview="list"`; the tiles of `grid` and `inline` draw the family icons described below.
566
652
 
567
653
  ```html
568
654
  <hub-file-input formControlName="attachments" [multiple]="true">
@@ -572,6 +658,73 @@ Customize it without forking the template: the `--hub-file-input-*` tokens (ever
572
658
  </hub-file-input>
573
659
  ```
574
660
 
661
+ #### Inline preview and stored files
662
+
663
+ `preview="inline"` puts the file inside the field. One tile fills it: the image when the browser can
664
+ paint it, otherwise the icon of its family and its name. Hover, keyboard focus or a drag over the tile
665
+ raise a "Replace" pill, and a button in the corner removes the file. With `multiple` the tiles form a
666
+ grid inside the field that ends in a tile for adding more, and `maxFiles` adds a "3 of 5 files"
667
+ counter; at the limit the add tile goes away.
668
+
669
+ `currentFile` shows what the record already has:
670
+
671
+ ```html
672
+ <hub-file-input
673
+ formControlName="logo"
674
+ label="Logo"
675
+ accept="image/*"
676
+ preview="inline"
677
+ [currentFile]="company.logoUrl"
678
+ (currentFileRemoved)="markForDeletion($event)"
679
+ />
680
+
681
+ <hub-file-input
682
+ formControlName="contract"
683
+ label="Signed contract"
684
+ preview="inline"
685
+ [currentFile]="{ url: '/api/contracts/42/file', name: 'contract.pdf', type: 'application/pdf' }"
686
+ />
687
+ ```
688
+
689
+ - A bare URL gives the name from its last segment, when that has an extension, and the type from a
690
+ `data:` URL. Pass a `HubCurrentFile` when the URL reveals neither, and a list with `multiple`.
691
+ - A stored file is only shown: the form value stays a `File`, a `File[]` or `null`. When the user
692
+ removes it, or replaces it with a picked file, `currentFileRemoved` emits it. That is the moment to
693
+ delete it on the server.
694
+ - A click on a tile opens its file: a picked image in a native `<dialog>`, a stored file or any other
695
+ picked file in a new tab. Delete or Backspace on a focused tile removes it.
696
+ - `readonly` keeps the files in view and openable but blocks every change. `[clearable]="false"` keeps
697
+ the user from removing files. `[imagePreview]="false"` draws every file as its icon and creates no
698
+ object URLs.
699
+
700
+ `preview="grid"` draws the same tiles under the dropzone. An avatar takes five tokens:
701
+
702
+ ```css
703
+ .avatar-field {
704
+ --hub-file-input-inline-width: 8rem;
705
+ --hub-file-input-inline-aspect-ratio: 1;
706
+ --hub-file-input-tile-radius: 50%;
707
+ --hub-file-input-tile-fit: cover;
708
+ --hub-file-input-tile-padding: 0;
709
+ }
710
+ ```
711
+
712
+ Each family icon (`pdf`, `document`, `spreadsheet`, `presentation`, `archive`, `audio`, `video`, `code`,
713
+ `image`, `generic`) is a mask token, so replacing one is one line, and the tile's `data-file-kind`
714
+ attribute scopes a colour to one family:
715
+
716
+ ```css
717
+ .my-form {
718
+ --hub-file-input-kind-pdf-icon: url('/icons/pdf.svg');
719
+ }
720
+
721
+ .my-form .hub-file-input__tile[data-file-kind='pdf'] {
722
+ --hub-file-input-kind-icon-color: #dc2626;
723
+ }
724
+ ```
725
+
726
+ The built-in drawings are [Bootstrap Icons](https://icons.getbootstrap.com) 1.13.1, under the MIT License.
727
+
575
728
  #### Reproducing your own dropzone
576
729
 
577
730
  The dropzone is built from a glyph, an invitation and a browse action, each themeable on its own — so a design system reproduces its own without forking the template.
@@ -630,6 +783,23 @@ form would have written anyway.
630
783
  The attribute is restricted to `<fieldset>` on purpose: on a `<div>` it would draw a legend over
631
784
  a group with none of the semantics assistive technology reads from a real fieldset.
632
785
 
786
+ #### One way to write the legend
787
+
788
+ `legend="…"` is shorthand: it builds a `<hub-legend>` for you. When the legend needs more than a
789
+ string — a required marker, an icon, a badge — project the element yourself and it is lifted into
790
+ the same native `<legend>`, with the same classes.
791
+
792
+ ```html
793
+ <fieldset hubFieldset [group]="form.controls.address">
794
+ <hub-legend [required]="true" [invalid]="form.controls.address.invalid">Shipping address</hub-legend>
795
+
796
+ </fieldset>
797
+ ```
798
+
799
+ The older `<ng-template hubLegend>` slot still works and is deprecated: it existed only so a legend
800
+ could carry markup, and `<hub-legend>` carries markup without an `ng-template` and without a second
801
+ directive to import. It is removed in 23.0.0.
802
+
633
803
  ### Validation states (invalid is automatic, valid is opt-in)
634
804
 
635
805
  The **invalid** state is always automatic: a touched, invalid field shows its
@@ -670,6 +840,18 @@ field once it is touched and valid. The invalid state is unaffected — it is al
670
840
  automatic; only success is gated behind this flag. A per-field `showValid` input
671
841
  overrides the global default.
672
842
 
843
+ `color` sets an application palette for every colour field without `swatches` of its own (there is
844
+ none by default) and the two accessible names the colour field adds: `customColorLabel`
845
+ (`'Custom color'`) for the grid's last cell and `pickerLabel` (`'Choose color'`) for the hex field's
846
+ square. `fileInput` carries the file-input labels, among them `removeFile(name)`, `open(name)`,
847
+ `replace`, `replaceFile(name)`, `close`, `count(count, max)` and `currentFile`.
848
+
849
+ ```ts
850
+ provideHubForms({
851
+ color: { swatches: HUB_COLOR_PALETTES.tailwind, customColorLabel: 'Other colour' }
852
+ });
853
+ ```
854
+
673
855
  ---
674
856
 
675
857
  ## 🎨 Styling
@@ -701,6 +883,11 @@ hub-input {
701
883
  }
702
884
  ```
703
885
 
886
+ **Colour field** — `--hub-input-color-size` is the width of the hex field's square; its height is
887
+ always the field's, and the default, the field's inner height, keeps it square. The grid of swatches
888
+ reads the `--hub-input-swatch-*` tokens, and `--hub-input-swatch-mark-color` forces one colour for every
889
+ check mark (unset, each mark is black or white, whichever reads on its swatch).
890
+
704
891
  **`hub-slider`** — `--hub-slider-track-fill` takes a full `<image>` (e.g. a `linear-gradient(to right, …)`) for the filled part of the track, which renders intact clipped to the current percentage; `--hub-slider-value-space` is the value-bubble headroom and collapses to `0` on a `[showValue]="false"` (flush) slider:
705
892
 
706
893
  ```css
@@ -747,7 +934,9 @@ import { HubSignalFieldControl, hubSignalErrorMessages } from 'ng-hub-ui-forms/s
747
934
 
748
935
  ## ♿ Accessibility
749
936
 
750
- - Labels are associated with their control (`for`/`id`); required fields are marked.
937
+ - Labels are associated with their control (`for`/`id`); required fields are marked. The colour
938
+ grid is a radio group, which a `<label for>` cannot name, so it points at the label's `id` through
939
+ `aria-labelledby`.
751
940
  - **`labelType="visually-hidden"` names a control that has no room for a visible label.** A
752
941
  toolbar search box or a compact grid cell cannot repeat the same word down every row, and the
753
942
  alternative was a control with no accessible name at all — a placeholder is not a name. The