fold-ng 0.7.0 → 0.9.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
@@ -287,9 +287,46 @@ radius / text / icon-size / space / motion / blur scales — see
287
287
  All standalone, signals-first, styled against the semantic tokens. Import from
288
288
  the package root.
289
289
 
290
+ ### Find one by what you need
291
+
292
+ The reference table below is keyed by component **name** — but you usually know
293
+ your **intent**, not the name (that's how an uppercase mini-title gets hand-rolled
294
+ instead of reaching for `fold-element-title`). Start here; **before hand-rolling a
295
+ label, field, badge, card or overlay, scan this table** — fold almost certainly
296
+ ships it.
297
+
298
+ | I need to… | Reach for |
299
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
300
+ | a small **uppercase label / eyebrow** over a group | `fold-element-title` (`variant="eyebrow"` · `bar` · `title`) |
301
+ | a **section** with a title + description + actions | `fold-page-section` (semantic `<section>` + `aria-labelledby`) |
302
+ | show **read-only label/value pairs** (a recap) | `fold-field-list` / `fold-field` (`dl/dt/dd`) |
303
+ | a **text** field | `fold-input` — number → `fold-number-input` · multiline note → `fold-textarea` |
304
+ | a **date** / **time** field | `fold-date` (date·datetime-local·month·week) · `fold-time` |
305
+ | a **dropdown** | `fold-select` (native) — custom rows → `fold-listbox` · multi → `fold-multiselect` |
306
+ | an **on/off** field · a **password** field | `fold-checkbox` · `fold-password-field` |
307
+ | a **range** / **debounced search** | `fold-slider` / `fold-range-slider` · `fold-search` |
308
+ | a **button** · **icon-only** action · icon **on/off** | `fold-button` (`<button>`/`<a>`) · `fold-button-icon` · `fold-toggle-icon` · text link → `fold-link` |
309
+ | an inline **"are you sure?"** guard | `fold-inline-confirm` (no modal — simple · type-to-confirm · secret) |
310
+ | a **"delete X" danger block** (type-to-confirm) | `fold-danger-zone` (alert frame + blast-radius text + retype-to-arm) |
311
+ | a **status / count pill** · status→colour | `fold-badge` · `fold-status-badge` |
312
+ | a **tinted message / alert** row | `fold-callout` (`inset` for in-flow) |
313
+ | a transient **toast** | `fold-toast` + `FoldToastService` |
314
+ | a **card** · titled info card · page **splash** | `fold-card` · `fold-context-card` · `fold-hero-section` (bordered → `fold-hero-card`) |
315
+ | in-page **tabs** · a routed **nav bar** · a segmented control | `fold-tabs` · `fold-view-nav` · `fold-view-toggle` / `fold-choice-row` |
316
+ | a **breadcrumb** trail (routerLink or href) | `fold-breadcrumb` (`[items]`, last = current page) |
317
+ | a **"← Back"** link (route, href, or history) | `fold-back-link` (`routerLink` / `href` / history-back button) |
318
+ | a **table** + **pagination** | `fold-data-table` · `fold-paginator` |
319
+ | an **avatar** · a cluster | `fold-avatar` (+ `…Detail`) · `fold-avatar-list` |
320
+ | an **empty** / **loading** state | `fold-empty-state` · `fold-loading` / `fold-spinner` |
321
+ | a **side panel** · anchored **popover** · actions **menu** | `fold-panel-host` · `fold-popover` · `fold-dropdown` |
322
+ | a **collapsible** section | `fold-disclosure` (a modal dialog is roadmap — use a modal `fold-panel-host` or `fold-inline-confirm`) |
323
+ | an **icon** · a **calendar** · a **timeline / stepper** | `fold-icon` · `fold-calendar-month`/`-week`/`-day`/`-list`/`-agenda`/`-timegrid` · `fold-timeline` |
324
+ | a **nav rail** · app **skeleton** · a **detail page** with rails | `fold-menu` · `fold-app-shell` · `fold-aside-layout` (page scaffold → `fold-page-layout`) |
325
+ | **drag-drop** file upload | `fold-file-dropzone` |
326
+
290
327
  | Component | Selector | What it is |
291
328
  | -------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
292
- | `FoldAppShellComponent` | `fold-app-shell` | Responsive app skeleton (rails + header + content + self-collapsing `footer` slots; `headerLayout`/`footerLayout` inset·full, `footerBehavior` pinned·scroll; `mobileNav` drawer·none — `[(mobileNavOpen)]` off-canvas drawer for the primary rail on mobile, or `none` to compose an `fold-nav-launcher`; `contentScroll` clip·auto; built-in skip-link to a focusable `<main>`. Regions float per-surface via `foldElevated`, not a shell flag). |
329
+ | `FoldAppShellComponent` | `fold-app-shell` | Responsive app skeleton (rails + header + content + self-collapsing `footer` slots; `headerLayout`/`footerLayout` inset·full, `footerBehavior` pinned·scroll; `mobileNav` drawer·none — `[(mobileNavOpen)]` off-canvas drawer for the primary rail on mobile, or `none` to compose an `fold-nav-launcher`; `scroll` scroll·stage — the shell owns the content scroll by default so pages flow (`[foldScrollRegion]` opts a nested area back into its own scroll); built-in skip-link to a focusable `<main>`. Regions float per-surface via `foldElevated`, not a shell flag). |
293
330
  | `FoldMenuComponent` (+ `Item` / `Section` / `Separator`) | `fold-menu` | Collapsible nav rail — coloured sections, `tint="follow"`, depth `level`, collapse-toggle placement. Items are `a[fold-menu-item]`. |
294
331
  | `FoldNavLauncherComponent` (+ `FoldNavTileComponent`) | `fold-nav-launcher` | Full-screen mobile nav launcher — a centred tile grid over a blurred scrim (scrim / `Escape` / close dismissal, focus-trap, scroll-lock). `columns="auto"` scales tiles to the count. Pairs with `fold-app-shell mobileNav="none"`. Tiles are `a[fold-nav-tile]` — `variant="surface"`·`filled`. |
295
332
  | `FoldPageLayoutComponent` | `fold-page-layout` | Page scaffold — gutter + header + body rhythm; fills its container (width is a content concern). Tokens `--fold-page-gutter` / `--fold-page-gap`; sections can `bleed` edge-to-edge. |
@@ -304,21 +341,26 @@ the package root.
304
341
  | `FoldFieldListComponent` / `…Field` | `fold-field-list` | Read-only `dl/dt/dd` recap — label/value pairs (`[empty]` placeholder). The _display_ half of a record; `fold-input` is the _edit_ half. |
305
342
  | `FoldInputComponent` | `fold-input` | Text-input control (`value: string`) — Signal Forms (`[formField]`) or standalone `[(value)]`; size × align × variant, `label` / `required` / `hint`. The _edit_ half of a record (`fold-field` reads). |
306
343
  | `FoldNumberInputComponent` | `fold-number-input` | Numeric sibling of `fold-input` (`value: number \| null`, empty ⇒ `null`); owns `min` / `max` / `step` + `label` / `required` / `hint`. Split so each control keeps its true type. |
344
+ | `FoldTextareaComponent` | `fold-textarea` | Multiline sibling of `fold-input` (`value: string`). **No resize handle** — fixed `rows` height, wraps + scrolls overflow. Shares the box + `label`/`required`/`hint`/`error` chrome. Signal Forms or `[(value)]`. |
345
+ | `FoldSelectComponent` | `fold-select` | Native `<select>` wrapper (options projected as `<option>`); shares `fold-input`'s box chrome. Signal Forms (`FormValueControl<string>`) or `[(value)]`. For custom rows use `fold-listbox`. |
346
+ | `FoldDateComponent` | `fold-date` | Native calendar-date wrapper (`type` = date · datetime-local · month · week) — keeps the OS picker, hands back a typed `[(value)]` string (`YYYY-MM-DD`). `min`/`max`/`step` pass through. Time-of-day → `fold-time`. |
347
+ | `FoldTimeComponent` | `fold-time` | Native time-of-day wrapper (`<input type="time">`) — typed `[(value)]` string (`HH:mm`), `min`/`max`/`step`. The sibling of `fold-date`. |
307
348
  | `FoldCheckboxComponent` | `fold-checkbox` | Boolean control — a native `<input type="checkbox">` (keyboard, `indeterminate`, forms) restyled to tokens. Signal Forms (`[formField]`, a `FormCheckboxControl`) or standalone `[(checked)]`; `indeterminate`, `label`/`ariaLabel`, `hint`/`errors`, `size`. |
308
349
  | `FoldPasswordFieldComponent` | `fold-password-field` | Password input + a live requirements checklist (a dot/tick per rule). Rules injected via `FoldPasswordRule` (`{ label, test }` — regex/zod/anything); `revealable` eye (a `fold-input` capability); `marker` dot/check; `[rules]` slot to redesign the list; `validChange`; Signal Forms. |
309
350
  | `FoldViewToggleComponent` | `fold-view-toggle` | Segmented single-select (Cards/Table, density, chart-mode…). Generic `options` (`{ value, icon?, label?, ariaLabel?, disabled? }`) + `[(value)]`; a real `role="radiogroup"` — roving tabindex, arrow keys, Home/End, disabled-skip; `size`, `iconOnly`, `activeStyle` (raised / accent). |
310
351
  | `FoldSearchComponent` | `fold-search` | Debounced search box — an `fold-input` that emits `searchChange` once typing settles (`delayMs`), trimmed + de-duplicated. |
311
352
  | `FoldListboxComponent` / `FoldOptionComponent` | `fold-listbox` / `fold-option` | Styleable single-select — the richer sibling of `fold-select` (native `<select>`) for options that need custom rows (icon, second line, status). On `fold-popover`; `role="listbox"` + `aria-activedescendant`, full keyboard (↑/↓, `Home`/`End`, type-ahead, `Enter`). Signal Forms (`FormValueControl<string>`, `[formField]`/`[(value)]`); shares `fold-input`'s box chrome. |
312
353
  | `FoldMultiselectComponent` | `fold-multiselect` | Multi-select sibling of `fold-listbox` (same popover + `fold-option` rows). Value is a set (`readonly string[]`); activating a row toggles it and the panel stays open. Separate component — not a `multiple` flag — so the Signal-Forms value type stays honest. `role="listbox"` + `aria-multiselectable`; the trigger summarises the picks. |
354
+ | `FoldOptgroupComponent` | `fold-optgroup` | Labelled group of `<fold-option>`s inside a `fold-listbox` / `fold-multiselect` — the styleable `<optgroup>`. Presentational: `role="group"` + `aria-labelledby` header (no `role="option"`, so keyboard nav skips it); the owner discovers grouped options in document order, so roving crosses groups seamlessly. |
313
355
  | `FoldSliderComponent` | `fold-slider` | Single-value range slider — a styled native `<input type="range">` (design-system track/fill/thumb). Signal Forms (`[formField]`, a `FormValueControl<number>`) or `[(value)]`; real `<label for>`, `aria-valuetext`, `hint`/`errors`, focus ring. |
314
356
  | `FoldRangeSliderComponent` | `fold-range-slider` | Dual-thumb range slider selecting a `{ min, max }` window (shares the slider track/thumb). Two-way `[(value)]`; a labelled `role="group"`, per-thumb i18n aria (`minLabel`/`maxLabel`) + formatted `aria-valuetext`, `disabled`. |
315
- | `FoldRangeSliderComponent` | `fold-range-slider` | Dual-thumb slider selecting a `{ min, max }` window over `[min, max]`; shares the slider track/fill/thumb. |
316
357
  | `FoldFileDropzoneComponent` | `fold-file-dropzone` | File-picker dropzone — drag-over visuals, keyboard activation, hidden `<input type=file>` plumbing; emits the picked `File[]` (presentational — never uploads). |
317
358
  | `FoldLinkComponent` | `fold-link` | Inline text link / link-button (icons, accent · muted). |
318
359
  | `FoldButtonComponent` | `button[foldButton]` · `a[foldButton]` | Action button — applied to a real `<button>` **or** `<a>` (link that looks like a button, gets `href`/`routerLink`); orthogonal `emphasis` (solid·soft·outline) × `intent` (primary·neutral·warning·danger) × 3 sizes × shape/`block`; `icon`/`iconTrailing` shorthand (auto-sized) or project content; `loading` (spinner + `aria-busy`). Use native `(click)`. |
319
360
  | `FoldButtonIconComponent` | `fold-button-icon` | Icon-only **momentary** button — shape × size × tone; a one-shot action (no pressed state). For a text button use `fold-button`; for on/off use `fold-toggle-icon`. |
320
361
  | `FoldToggleIconComponent` | `fold-toggle-icon` | Icon-only **toggle** — the same surface as `fold-button-icon`, plus `[(active)]` + `aria-pressed` (true/false) and a pressed state. Emits `toggled`. |
321
362
  | `FoldInlineConfirmComponent` | `fold-inline-confirm` | In-place “are you sure?” guard — the projected trigger swaps to a confirm/cancel row (no modal). Simple (`confirmed` emits `""`), type-to-confirm (`[match]`), or secret (`password`, masked, emits the value). `confirmIcon` + a chosen `cancelIcon`; `Escape` cancels; `message` announced via `aria-describedby`; focus in-then-back; controlled `[(open)]` + `keepOpenOnConfirm` for async pending; i18n via `provideFoldInlineConfirmLabels`. |
363
+ | `FoldDangerZoneComponent` | `fold-danger-zone` | Framed destructive-action block for “delete X” settings. `appearance="filled"` (alert-tinted) or `"section"` (a danger section: alert border + normal-background body for ordinary content). The confirm **reveals on click** — an `actionLabel` button opens an in-place `fold-inline-confirm` (plain “are you sure?”, or type-to-confirm when `confirmPhrase` is set); `(confirmed)` emits the typed text. Omit `actionLabel` for a section with no action. `role="group"` + `aria-labelledby`. |
322
364
  | `FoldDataTableComponent` | `fold-data-table` | Controlled roster table — sortable sticky header, tone rows, controlled selection (checkbox column), roving-keyboard nav, `mobileLayout` (scroll / auto-cards / custom `foldRowCard`), an optional `foldToolbar` bar, sticky-first, density. |
323
365
  | `FoldPaginatorComponent` | `fold-paginator` | Server-side paginator (size selector + range + page nav). |
324
366
  | `FoldTimelineComponent` | `fold-timeline` | Connected rail of nodes (dot + optional date + label) — `vertical` navigable history or `horizontal` step progress; nodes optionally clickable. |
@@ -326,8 +368,10 @@ the package root.
326
368
  | `FoldStatusBadgeComponent` | `fold-status-badge` | Status→colour badge (maps a domain status key to a tone). |
327
369
  | `FoldChoiceRowComponent` | `fold-choice-row` | Segmented / chip selector. |
328
370
  | `FoldViewNavComponent` | `fold-view-nav` | Navigation bar styled as tabs. Items carry a `link` (routerLink → a real `<a>`: cmd-click, deep-links, active state auto), an `href`, or nothing (a button); `aria-current="page"` on the active one. `direction="auto"` follows a wrapping `fold-nav-layout`; `collapsed` for an icon rail. For in-page panel switching use `fold-tabs` instead. |
371
+ | `FoldBreadcrumbComponent` | `fold-breadcrumb` | Hierarchical link trail. Data-driven `[items]` where each crumb links by `routerLink` **or** `href` (works without the router; `RouterLink` only instantiates on a `routerLink` crumb). Last item is the current page (`aria-current="page"`), not a link. `navigation` landmark, decorative chevron separators. Needs `@angular/router` only when a crumb uses `routerLink` (optional peer). |
372
+ | `FoldBackLinkComponent` | `fold-back-link` | The “← Back” affordance for a detail page. Three modes by input: `routerLink` (in-app), `href` (external / non-router), or neither → a history-back `<button>` (`Location.back()`). Router-coupled but degradable — the history mode needs no router. `label` + leading `icon` (default `chevron-left`). |
329
373
  | `FoldTabsComponent` + `FoldTabPanelComponent` | `fold-tabs` + `fold-tab-panel` | The in-page ARIA Tabs widget: `role="tablist"` + roving arrow-key keyboard, `aria-selected`/`aria-orientation`, each tab wired to its `fold-tab-panel` (`aria-controls` ↔ `aria-labelledby`). Panels take the bar by ref (`[tabs]="t"`) so they coordinate across `fold-nav-layout` slots. |
330
- | `FoldIconComponent` | `fold-icon` | SVG icon (114-icon built-in set + `FoldIconRegistry`). |
374
+ | `FoldIconComponent` | `fold-icon` | SVG icon (~135 built-in glyphs across 7 categories incl. `commerce` + `FoldIconRegistry`). |
331
375
  | `FoldSpinnerComponent` | `fold-spinner` | Indeterminate loading arc (`currentColor`, icon-sized, reduced-motion aware). Decorative by default; `label` → `role="status"`. Powers `loading` on the buttons. |
332
376
  | `FoldAvatarComponent` / `…Detail` | `fold-avatar` | Initials/image avatar (square, muted, status ring) + identity cell. |
333
377
  | `FoldAvatarListComponent` | `fold-avatar-list` | Overlapping avatar cluster (per-face variant, `limit` + a `+N` overflow chip). |
@@ -338,16 +382,71 @@ the package root.
338
382
  | `FoldDisclosureComponent` | `fold-disclosure` | One summary toggling one collapsible panel — the accordion primitive (open-state is the consumer's to bind); keeps content mounted, unlike native `<details>`. |
339
383
  | `FoldPanelHostComponent` | `fold-panel-host` | Side-panel / overlay host (+ `FoldPanelHostService` / `FoldPanelRef` / `FoldPanelToggle`). Modal: accessible name, `inert` background barrier, top-most focus trap, scroll-lock. Localise the close label once via `provideFoldPanelLabels({ close })`. |
340
384
  | `FoldPanelHeaderComponent` | `fold-panel-header` | Standard panel header (title/eyebrow, self-closing). Names its dialog (`aria-labelledby`) and reads the app-wide close label. |
385
+ | `FoldPanelFooterComponent` | `fold-panel-footer` | Panel/dialog action bar pairing with `fold-panel-header`: tokenised top border + padding + `align="end" \| "between" \| "start"`. Projects the buttons; sits `flex: none` so it stays pinned to the panel bottom while the body scrolls (no `position: sticky`). |
341
386
  | `FoldPopoverComponent` | `fold-popover` | Anchored floating layer — projected content in the native top layer (escapes `overflow`/`z-index`), positioned by a dependency-free **flip → size → shift** engine (`computePlacement`): a tall panel gets a `max-height` and scrolls inside the viewport. `[(open)]`; `autoUpdate` (ResizeObserver); optional `arrow`; native CSS enter/exit (`@starting-style` + `allow-discrete`); outside-click + `Escape` dismissal, focus-return, auto-wired `aria-haspopup`/`expanded`/`controls`. |
342
387
  | `FoldDropdownComponent` | `fold-dropdown` | Actions menu on `fold-popover` — `role="menu"` with `<fold-dropdown-item>`s, ↑/↓ roving, `Home`/`End`, type-ahead; opens onto the first enabled item, closes returning focus to the trigger. Give the trigger `foldPopoverTrigger="menu"`. |
343
388
 
389
+ | `FoldCalendarMonthComponent` | `fold-calendar-month` | Month grid where events **span** the days they cover: a date-axis `role="grid"` with one roving tab stop, and over it a lane-packed layer of bands (one per week crossed, with open edges). `+N` chips sit on the crowded day, not at the end of the row. |
390
+ | `FoldCalendarWeekComponent` | `fold-calendar-week` | Seven day columns of stacked chips — nothing spans, so nothing is clipped and every chip is a real button in the tab order. Container-queried: labels and icons drop out before they truncate. |
391
+ | `FoldCalendarDayComponent` | `fold-calendar-day` | One day in full, with room for the subline — where a `dayClick` drill-down lands. Empty state takes a projected action (`button[empty]`). |
392
+ | `FoldCalendarListComponent` | `fold-calendar-list` | The flat reading of the same feed, in date order, each row leading with the span `Intl` formats. Nothing is ever hidden behind a lane budget. |
393
+ | `FoldCalendarTimegridComponent` | `fold-calendar-timegrid` | Hour columns for a week or a day, with the all-day strip on top. Timed events are placed on the clock and share their width only with what they actually collide with (exclusive boundaries, per-cluster widening); a span crossing midnight becomes one block per day. Times are wall-clock `HH:mm`, never instants; `now` is an input, not a clock. |
394
+ | `FoldCalendarAgendaComponent` | `fold-calendar-agenda` | "What's next" rail grouped by day, with a **to-handle** slice (the `warning`/`alert` tones — the same scale the chips paint with) and a live badge. Collapses to a spine; `mode`/`collapsed` are models, so the app owns persistence. |
395
+ | `FoldCalendarToolbarComponent` | `fold-calendar-toolbar` | Today / prev / next / period title / view switch. Owns no data — `date` and `view` are two-way, and the step matches the reading. `views` takes `{ value, label }` for an app's own view. |
396
+ | `FoldCalendarSourceFilterComponent` | `fold-calendar-source-filter` | Chips switching each feed of a merged calendar on and off. Owns the **selection** only; the caller runs the pure `foldFilterBySource()`. |
397
+
398
+ **Two tiers, on purpose.** The components are the first; under them, the same
399
+ geometry is exported as pure functions — `foldBuildMonthGrid` (week rows with
400
+ their events already packed into lanes), `foldBuildWeek` / `foldBuildDay`,
401
+ `foldBuildAgenda`, plus `foldShiftDate` / `foldRangeForView` / `foldViewTitle`
402
+ for paging and naming a period, and `foldCalendarNextFocus` for the arrow keys.
403
+ Lay a calendar out without rendering it with these components, and the hard part
404
+ — packing spans into lanes, clipping them at week boundaries, keeping an open
405
+ edge on the side that continues — is already solved and already tested. The
406
+ filters (`foldEventsOnDay`, `foldEventsInRange`, `foldFilterBySource`) are the
407
+ tier every page uses whichever rendering it picks.
408
+
409
+ **Measured, and kept measured.** Laying out a month (mean of 20 runs,
410
+ `pnpm bench:calendar`, committed with a budget so it cannot regress quietly):
411
+
412
+ | events in the window | 50 | 200 | 1 000 | 5 000 | 20 000 |
413
+ | -------------------- | ---- | ---- | ----- | ----- | ------ |
414
+ | ms per layout | 0.14 | 0.41 | 0.99 | 5.0 | 22 |
415
+
416
+ Calendar dates are plain `YYYY-MM-DD` **strings**, never `Date`. That is also
417
+ exactly what `Temporal.PlainDate.toString()` returns, so the family is
418
+ **Temporal-native without depending on it**: `foldFromTemporal()` accepts a
419
+ `PlainDate`, `PlainDateTime` or `ZonedDateTime` (structurally typed, so it
420
+ compiles on a runtime that has none of them), and going the other way needs no
421
+ helper — `Temporal.PlainDate.from(foldDate)` already takes one of ours. See
422
+ `foldToday` / `foldFromNativeDate` / `foldAddDays` and the reasoning in
423
+ [`/calendar-dates`](https://hugoheynard.github.io/fold-ng/calendar-dates). The
424
+ family plots on the **Gregorian** calendar and has no resource (staff × day)
425
+ view. Whole-day spans are the month, week, day, list and agenda views; the
426
+ **time grid** adds the hours, with time as wall-clock `HH:mm` rather than an
427
+ instant — the app converts at its own boundary, and no zone can move a meeting
428
+ after that.
429
+
430
+ It is a **pure display**, by decision and not by omission: it knows nothing
431
+ about what an event means or which layer produced it, so it never originates
432
+ one — no drag-to-create, no range-select. Tone is the caller's to compute,
433
+ which is what lets the same component paint a leave request and an account
434
+ sitting open between registration and activation, its band crossing into
435
+ `warning` then `alert` on a threshold the app owns. Everything drawn around an event is a projectable template:
436
+ `foldCalendarEvent`, `foldCalendarDay`, `foldCalendarHeading`,
437
+ `foldCalendarTitle`, `foldCalendarOverflow`.
438
+
344
439
  Directives worth knowing: **`foldSurface`** (`page`·`chrome` — the seam a mixed
345
440
  theme re-colours across), **`foldElevated`** (raise any bg-owning element into
346
441
  an inset, rounded, shadowed card — the per-surface "floating" mechanism, driven
347
442
  by `--fold-surface-inset`/`-radius`/`-shadow`), **`foldStickyColumn`** (turn an
348
443
  `<aside>` into a sticky side column — `sticky="top·center·bottom"` + `stickyOffset`,
349
444
  the rest tunable via `--fold-sticky-column-*`; un-stick with
350
- `--fold-sticky-column-position: static` at the page's stacking breakpoint), and
445
+ `--fold-sticky-column-position: static` at the page's stacking breakpoint),
446
+ **`foldScrollRegion`** (turn any element into a bounded, coordinated scroll box —
447
+ `overflow` + `min-*: 0` + `overscroll-behavior` + the house scrollbar, on a
448
+ `block`·`inline`·`both` axis; the one opt-in of the shell scroll system, it
449
+ registers with the shell so an overlay freezes it while a modal is open), and
351
450
  **`foldRepeatPress`** (press-and-hold auto-repeat for a stepper button — fires
352
451
  once on press then on a tunable cadence while held, and stops the instant
353
452
  `foldRepeatPressDisabled` goes true mid-hold).
@@ -0,0 +1,4 @@
1
+ {
2
+ "module": "../fesm2022/fold-ng-src-devtools.mjs",
3
+ "typings": "../types/fold-ng-src-devtools.d.ts"
4
+ }
@@ -0,0 +1,218 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, output, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { FOLD_BUILTIN_ICON_CATEGORIES, FoldIconRegistry, FoldIconComponent, FoldButtonIconComponent } from 'fold-ng';
4
+
5
+ const SIZE_STEPS = ["xs", "sm", "md", "lg", "xl"];
6
+ /** Bucket id for registry icons that belong to no built-in category. */
7
+ const CUSTOM_CATEGORY_ID = "custom";
8
+ /** name → built-in category id, for bucketing the live registry by theme. */
9
+ const CATEGORY_OF_NAME = new Map(FOLD_BUILTIN_ICON_CATEGORIES.flatMap((category) => category.names.map((name) => [name, category.id])));
10
+ /**
11
+ * `<fold-icon-devtool>` — a **dev-only** floating panel that browses the live
12
+ * {@link FoldIconRegistry} (built-ins + whatever the host app has registered),
13
+ * with a search, a preview grid, and a mini playground that builds a
14
+ * `<fold-icon>` snippet and copies it to the clipboard. The panel is **draggable**
15
+ * (grab the header) and **minimisable** (collapses to a pill).
16
+ *
17
+ * **Ship it behind a dev guard.** It is published from the opt-in `fold-ng/devtools`
18
+ * entry so it never lands in a bundle that doesn't ask for it; import it only on a
19
+ * dev path so a production build tree-shakes it away:
20
+ *
21
+ * ```ts
22
+ * import { isDevMode, signal } from "@angular/core";
23
+ * protected readonly showIconTool = signal(false);
24
+ * // template: @if (showIconTool()) { <fold-icon-devtool (closed)="showIconTool.set(false)" /> }
25
+ * // …and only reach the import behind `if (isDevMode())`.
26
+ * ```
27
+ *
28
+ * @selector `fold-icon-devtool`
29
+ */
30
+ class FoldIconDevtoolComponent {
31
+ registry = inject(FoldIconRegistry);
32
+ /** Emitted when the user closes the panel — the host should stop rendering it. */
33
+ closed = output();
34
+ sizeSteps = SIZE_STEPS;
35
+ /** Collapsed to a pill (header only). */
36
+ minimized = signal(false, /* @ts-ignore */
37
+ ...(ngDevMode ? [{ debugName: "minimized" }] : /* istanbul ignore next */ []));
38
+ /** Free-drag position; `null` = docked in the default corner (CSS). */
39
+ pos = signal(null, /* @ts-ignore */
40
+ ...(ngDevMode ? [{ debugName: "pos" }] : /* istanbul ignore next */ []));
41
+ /** The live registry names (built-ins + host-registered), sorted. */
42
+ allNames = computed(() => this.registry.names(), /* @ts-ignore */
43
+ ...(ngDevMode ? [{ debugName: "allNames" }] : /* istanbul ignore next */ []));
44
+ query = signal("", /* @ts-ignore */
45
+ ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
46
+ /** Names matching the search (case-insensitive substring). */
47
+ matches = computed(() => {
48
+ const q = this.query().trim().toLowerCase();
49
+ const names = this.allNames();
50
+ return q ? names.filter((n) => n.includes(q)) : names;
51
+ }, /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "matches" }] : /* istanbul ignore next */ []));
53
+ /** Category ids the user has collapsed (folded to their header). */
54
+ collapsed = signal(new Set(), /* @ts-ignore */
55
+ ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
56
+ /**
57
+ * The matches grouped into category sections, in built-in order, with a
58
+ * trailing **Custom** bucket for host-registered icons. Empty categories are
59
+ * dropped so a search only shows the themes it actually hit.
60
+ */
61
+ categories = computed(() => {
62
+ const buckets = new Map();
63
+ for (const name of this.matches()) {
64
+ const id = CATEGORY_OF_NAME.get(name) ?? CUSTOM_CATEGORY_ID;
65
+ const bucket = buckets.get(id);
66
+ if (bucket) {
67
+ bucket.push(name);
68
+ }
69
+ else {
70
+ buckets.set(id, [name]);
71
+ }
72
+ }
73
+ const sections = [];
74
+ for (const category of FOLD_BUILTIN_ICON_CATEGORIES) {
75
+ const names = buckets.get(category.id);
76
+ if (names) {
77
+ sections.push({ id: category.id, label: category.label, names });
78
+ }
79
+ }
80
+ const custom = buckets.get(CUSTOM_CATEGORY_ID);
81
+ if (custom) {
82
+ sections.push({ id: CUSTOM_CATEGORY_ID, label: "Custom", names: custom });
83
+ }
84
+ return sections;
85
+ }, /* @ts-ignore */
86
+ ...(ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []));
87
+ /** Whether a category section is folded to its header. */
88
+ isCollapsed(id) {
89
+ return this.collapsed().has(id);
90
+ }
91
+ toggleCategory(id) {
92
+ this.collapsed.update((set) => {
93
+ const next = new Set(set);
94
+ if (!next.delete(id)) {
95
+ next.add(id);
96
+ }
97
+ return next;
98
+ });
99
+ }
100
+ /** The icon selected into the playground (empty = none). */
101
+ selected = signal("", /* @ts-ignore */
102
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
103
+ /** Playground size preset. */
104
+ size = signal("md", /* @ts-ignore */
105
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
106
+ /** The snippet the copy button writes — size only emitted when non-default. */
107
+ snippet = computed(() => {
108
+ const name = this.selected();
109
+ if (!name) {
110
+ return "";
111
+ }
112
+ const size = this.size();
113
+ return size === "md"
114
+ ? `<fold-icon name="${name}" />`
115
+ : `<fold-icon name="${name}" size="${size}" />`;
116
+ }, /* @ts-ignore */
117
+ ...(ngDevMode ? [{ debugName: "snippet" }] : /* istanbul ignore next */ []));
118
+ /** Flashes true briefly after a successful copy. */
119
+ copied = signal(false, /* @ts-ignore */
120
+ ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
121
+ select(name) {
122
+ this.selected.set(name);
123
+ }
124
+ setSize(size) {
125
+ this.size.set(size);
126
+ }
127
+ onSearch(event) {
128
+ const target = event.target;
129
+ if (target instanceof HTMLInputElement) {
130
+ this.query.set(target.value);
131
+ }
132
+ }
133
+ copy() {
134
+ const snippet = this.snippet();
135
+ if (!snippet) {
136
+ return;
137
+ }
138
+ void navigator.clipboard.writeText(snippet).then(() => {
139
+ this.copied.set(true);
140
+ setTimeout(() => this.copied.set(false), 1200);
141
+ });
142
+ }
143
+ toggleMinimize() {
144
+ this.minimized.update((m) => !m);
145
+ }
146
+ close() {
147
+ this.closed.emit();
148
+ }
149
+ // ── Drag (grab the header) ────────────────────────────────────────────
150
+ dragDx = 0;
151
+ dragDy = 0;
152
+ onDragStart(event) {
153
+ const target = event.target;
154
+ const handle = event.currentTarget;
155
+ if (!(target instanceof HTMLElement) || !(handle instanceof HTMLElement)) {
156
+ return;
157
+ }
158
+ // Ignore drags that start on an interactive header control.
159
+ if (target.closest("button")) {
160
+ return;
161
+ }
162
+ const host = handle.closest("fold-icon-devtool");
163
+ if (!host) {
164
+ return;
165
+ }
166
+ const rect = host.getBoundingClientRect();
167
+ this.dragDx = event.clientX - rect.left;
168
+ this.dragDy = event.clientY - rect.top;
169
+ handle.setPointerCapture(event.pointerId);
170
+ event.preventDefault();
171
+ }
172
+ onDragMove(event) {
173
+ const handle = event.currentTarget;
174
+ if (!(handle instanceof HTMLElement) ||
175
+ !handle.hasPointerCapture(event.pointerId)) {
176
+ return;
177
+ }
178
+ const width = window.innerWidth;
179
+ const height = window.innerHeight;
180
+ const x = Math.max(0, Math.min(event.clientX - this.dragDx, width - 40));
181
+ const y = Math.max(0, Math.min(event.clientY - this.dragDy, height - 40));
182
+ this.pos.set({ x, y });
183
+ }
184
+ onDragEnd(event) {
185
+ const handle = event.currentTarget;
186
+ if (handle instanceof HTMLElement &&
187
+ handle.hasPointerCapture(event.pointerId)) {
188
+ handle.releasePointerCapture(event.pointerId);
189
+ }
190
+ }
191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldIconDevtoolComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
192
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: FoldIconDevtoolComponent, isStandalone: true, selector: "fold-icon-devtool", outputs: { closed: "closed" }, host: { attributes: { "role": "dialog", "aria-label": "Icon devtool" }, properties: { "class.minimized": "minimized()", "style.left.px": "pos()?.x ?? null", "style.top.px": "pos()?.y ?? null", "class.docked": "pos() === null" } }, ngImport: i0, template: "<header\n class=\"dt-head\"\n (pointerdown)=\"onDragStart($event)\"\n (pointermove)=\"onDragMove($event)\"\n (pointerup)=\"onDragEnd($event)\"\n (pointercancel)=\"onDragEnd($event)\"\n>\n <span class=\"dt-grip\" aria-hidden=\"true\"></span>\n <span class=\"dt-title\">\n <fold-icon name=\"grid\" size=\"sm\" aria-hidden=\"true\" />\n Icons\n <span class=\"dt-count\">{{ matches().length }}</span>\n </span>\n <span class=\"dt-actions\">\n <fold-button-icon\n [icon]=\"minimized() ? 'chevron-up' : 'minus'\"\n size=\"sm\"\n [tooltip]=\"minimized() ? 'Expand' : 'Minimise'\"\n (clicked)=\"toggleMinimize()\"\n />\n <fold-button-icon\n icon=\"close\"\n size=\"sm\"\n tooltip=\"Close\"\n (clicked)=\"close()\"\n />\n </span>\n</header>\n\n@if (!minimized()) {\n <div class=\"dt-body\">\n <div class=\"dt-search\">\n <fold-icon\n name=\"search\"\n size=\"sm\"\n class=\"dt-search-ico\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n class=\"dt-search-input\"\n placeholder=\"Search icons\u2026\"\n aria-label=\"Search icons\"\n [value]=\"query()\"\n (input)=\"onSearch($event)\"\n />\n </div>\n\n <div class=\"dt-cats\">\n @for (cat of categories(); track cat.id) {\n <section class=\"dt-cat\">\n <button\n type=\"button\"\n class=\"dt-cat-head\"\n [attr.aria-expanded]=\"!isCollapsed(cat.id)\"\n (click)=\"toggleCategory(cat.id)\"\n >\n <fold-icon\n [name]=\"isCollapsed(cat.id) ? 'chevron-right' : 'chevron-down'\"\n size=\"sm\"\n aria-hidden=\"true\"\n />\n <span class=\"dt-cat-label\">{{ cat.label }}</span>\n <span class=\"dt-cat-count\">{{ cat.names.length }}</span>\n </button>\n\n @if (!isCollapsed(cat.id)) {\n <div class=\"dt-grid\" role=\"listbox\" [attr.aria-label]=\"cat.label\">\n @for (name of cat.names; track name) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"dt-cell\"\n [class.is-selected]=\"selected() === name\"\n [attr.aria-selected]=\"selected() === name\"\n [title]=\"name\"\n (click)=\"select(name)\"\n >\n <fold-icon [name]=\"name\" size=\"md\" aria-hidden=\"true\" />\n </button>\n }\n </div>\n }\n </section>\n } @empty {\n <p class=\"dt-empty\">No icon matches \u201C{{ query() }}\u201D.</p>\n }\n </div>\n\n @if (selected(); as name) {\n <div class=\"dt-play\">\n <div class=\"dt-preview\">\n <fold-icon [name]=\"name\" [size]=\"size()\" />\n <code class=\"dt-name\">{{ name }}</code>\n </div>\n <div class=\"dt-controls\">\n <div class=\"dt-sizes\" role=\"group\" aria-label=\"Preview size\">\n @for (s of sizeSteps; track s) {\n <button\n type=\"button\"\n class=\"dt-size\"\n [class.is-on]=\"size() === s\"\n (click)=\"setSize(s)\"\n >\n {{ s }}\n </button>\n }\n </div>\n <div class=\"dt-snippet\">\n <code>{{ snippet() }}</code>\n <button\n type=\"button\"\n class=\"dt-copy\"\n [class.is-copied]=\"copied()\"\n (click)=\"copy()\"\n >\n <fold-icon\n [name]=\"copied() ? 'check' : 'copy'\"\n size=\"sm\"\n aria-hidden=\"true\"\n />\n {{ copied() ? \"Copied\" : \"Copy\" }}\n </button>\n </div>\n </div>\n </div>\n } @else {\n <p class=\"dt-hint\">\n Pick an icon to build a <code>&lt;fold-icon&gt;</code> snippet.\n </p>\n }\n </div>\n}\n", styles: [":host{position:fixed;z-index:2147483000;display:flex;flex-direction:column;width:320px;max-width:calc(100vw - 2rem);max-height:min(78vh,640px);overflow:hidden;background:var(--fold-color-surface-card);color:var(--fold-color-text);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-lg, 14px);box-shadow:var(--fold-shadow-lg);font-size:var(--fold-text-sm, .875rem);-webkit-user-select:none;user-select:none}:host(.docked){right:1.25rem;bottom:1.25rem}:host(.minimized){max-height:none}.dt-head{display:flex;align-items:center;gap:var(--fold-space-sm, .5rem);padding:.4rem .5rem .4rem .75rem;background:var(--fold-color-surface-raised);border-bottom:1px solid var(--fold-color-border-subtle);cursor:grab;touch-action:none}.dt-head:active{cursor:grabbing}:host(.minimized) .dt-head{border-bottom:0}.dt-grip{width:10px;height:14px;flex:none;background-image:radial-gradient(currentColor 1px,transparent 1.4px);background-size:4px 4px;color:var(--fold-color-text-faded);opacity:.7}.dt-title{display:flex;align-items:center;gap:.4rem;font-weight:600}.dt-count{padding:.05rem .4rem;border-radius:var(--fold-radius-pill, 9999px);background:var(--fold-color-surface-sunken);color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem);font-variant-numeric:tabular-nums}.dt-actions{display:flex;align-items:center;gap:.15rem;margin-left:auto}.dt-body{display:flex;flex-direction:column;gap:var(--fold-space-sm, .6rem);padding:var(--fold-space-sm, .6rem);min-height:0;overflow:hidden}.dt-search{position:relative;flex:none}.dt-search-ico{position:absolute;top:50%;left:.55rem;transform:translateY(-50%);color:var(--fold-color-text-muted);pointer-events:none}.dt-search-input{width:100%;box-sizing:border-box;padding:.4rem .6rem .4rem 1.9rem;font:inherit;color:var(--fold-color-text);background:var(--fold-color-surface-sunken);border:1px solid var(--fold-color-border-subtle);border-radius:var(--fold-radius-sm, 8px)}.dt-search-input::placeholder{color:var(--fold-color-text-faded)}.dt-search-input:focus-visible{outline:none;border-color:var(--fold-color-primary);box-shadow:0 0 0 2px var(--fold-color-primary-border)}.dt-cats{display:flex;flex-direction:column;gap:.15rem;overflow-y:auto;min-height:4rem;padding:.1rem}.dt-cat{display:flex;flex-direction:column}.dt-cat-head{all:unset;position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:.35rem;padding:.3rem .35rem;background:var(--fold-color-surface-card);color:var(--fold-color-text-muted);cursor:pointer}.dt-cat-head:hover{color:var(--fold-color-text)}.dt-cat-head:focus-visible{outline:2px solid var(--fold-color-primary-border);outline-offset:-2px;border-radius:var(--fold-radius-sm, 8px)}.dt-cat-label{font-size:var(--fold-text-xs, .75rem);font-weight:600;text-transform:uppercase;letter-spacing:.04em}.dt-cat-count{padding:.02rem .35rem;border-radius:var(--fold-radius-pill, 9999px);background:var(--fold-color-surface-sunken);color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem);font-variant-numeric:tabular-nums}.dt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(40px,1fr));gap:.25rem;padding:.1rem .1rem .35rem}.dt-cell{all:unset;display:grid;place-items:center;aspect-ratio:1;border-radius:var(--fold-radius-sm, 8px);color:var(--fold-color-text-secondary);cursor:pointer;transition:background var(--fold-motion-fast)}.dt-cell:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.dt-cell:focus-visible{outline:2px solid var(--fold-color-primary-border);outline-offset:-2px}.dt-cell.is-selected{background:var(--fold-color-primary-surface, var(--fold-color-surface-hover));color:var(--fold-color-primary);box-shadow:inset 0 0 0 1px var(--fold-color-primary-border)}.dt-empty,.dt-hint{margin:0;padding:.5rem .25rem;color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem)}.dt-play{flex:none;display:flex;flex-direction:column;gap:.5rem;padding-top:.5rem;border-top:1px solid var(--fold-color-border-subtle)}.dt-preview{display:flex;align-items:center;gap:.6rem;color:var(--fold-color-text)}.dt-name{font-family:ui-monospace,monospace;font-size:var(--fold-text-xs, .75rem);color:var(--fold-color-text-muted)}.dt-controls{display:flex;flex-direction:column;gap:.4rem}.dt-sizes{display:inline-flex;gap:.15rem;padding:.15rem;background:var(--fold-color-surface-sunken);border-radius:var(--fold-radius-sm, 8px);width:fit-content}.dt-size{all:unset;padding:.15rem .45rem;border-radius:var(--fold-radius-xs, 5px);color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem);cursor:pointer}.dt-size:hover{color:var(--fold-color-text)}.dt-size.is-on{background:var(--fold-color-surface-card);color:var(--fold-color-text);box-shadow:var(--fold-shadow-sm)}.dt-size:focus-visible{outline:2px solid var(--fold-color-primary-border)}.dt-snippet{display:flex;align-items:center;gap:.4rem}.dt-snippet code{flex:1 1 auto;min-width:0;overflow-x:auto;white-space:nowrap;padding:.35rem .5rem;background:var(--fold-color-surface-sunken);border-radius:var(--fold-radius-sm, 8px);font-family:ui-monospace,monospace;font-size:var(--fold-text-xs, .75rem);color:var(--fold-color-text)}.dt-copy{all:unset;display:inline-flex;align-items:center;gap:.3rem;flex:none;padding:.35rem .6rem;border-radius:var(--fold-radius-sm, 8px);background:var(--fold-color-primary);color:var(--fold-color-on-primary);font-size:var(--fold-text-xs, .75rem);font-weight:600;cursor:pointer}.dt-copy:hover{background:var(--fold-color-primary-strong, var(--fold-color-primary))}.dt-copy:focus-visible{outline:2px solid var(--fold-color-primary-border);outline-offset:2px}.dt-copy.is-copied{background:var(--fold-color-success, var(--fold-color-primary))}@media(prefers-reduced-motion:reduce){.dt-cell{transition:none}}\n"], dependencies: [{ kind: "component", type: FoldIconComponent, selector: "fold-icon", inputs: ["name", "size", "tone", "title"] }, { kind: "component", type: FoldButtonIconComponent, selector: "fold-button-icon", inputs: ["icon", "shape", "size", "tone", "disabled", "loading", "tooltip", "type"], outputs: ["clicked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
193
+ }
194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FoldIconDevtoolComponent, decorators: [{
195
+ type: Component,
196
+ args: [{ selector: "fold-icon-devtool", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [FoldIconComponent, FoldButtonIconComponent], host: {
197
+ role: "dialog",
198
+ "aria-label": "Icon devtool",
199
+ "[class.minimized]": "minimized()",
200
+ "[style.left.px]": "pos()?.x ?? null",
201
+ "[style.top.px]": "pos()?.y ?? null",
202
+ "[class.docked]": "pos() === null",
203
+ }, template: "<header\n class=\"dt-head\"\n (pointerdown)=\"onDragStart($event)\"\n (pointermove)=\"onDragMove($event)\"\n (pointerup)=\"onDragEnd($event)\"\n (pointercancel)=\"onDragEnd($event)\"\n>\n <span class=\"dt-grip\" aria-hidden=\"true\"></span>\n <span class=\"dt-title\">\n <fold-icon name=\"grid\" size=\"sm\" aria-hidden=\"true\" />\n Icons\n <span class=\"dt-count\">{{ matches().length }}</span>\n </span>\n <span class=\"dt-actions\">\n <fold-button-icon\n [icon]=\"minimized() ? 'chevron-up' : 'minus'\"\n size=\"sm\"\n [tooltip]=\"minimized() ? 'Expand' : 'Minimise'\"\n (clicked)=\"toggleMinimize()\"\n />\n <fold-button-icon\n icon=\"close\"\n size=\"sm\"\n tooltip=\"Close\"\n (clicked)=\"close()\"\n />\n </span>\n</header>\n\n@if (!minimized()) {\n <div class=\"dt-body\">\n <div class=\"dt-search\">\n <fold-icon\n name=\"search\"\n size=\"sm\"\n class=\"dt-search-ico\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n class=\"dt-search-input\"\n placeholder=\"Search icons\u2026\"\n aria-label=\"Search icons\"\n [value]=\"query()\"\n (input)=\"onSearch($event)\"\n />\n </div>\n\n <div class=\"dt-cats\">\n @for (cat of categories(); track cat.id) {\n <section class=\"dt-cat\">\n <button\n type=\"button\"\n class=\"dt-cat-head\"\n [attr.aria-expanded]=\"!isCollapsed(cat.id)\"\n (click)=\"toggleCategory(cat.id)\"\n >\n <fold-icon\n [name]=\"isCollapsed(cat.id) ? 'chevron-right' : 'chevron-down'\"\n size=\"sm\"\n aria-hidden=\"true\"\n />\n <span class=\"dt-cat-label\">{{ cat.label }}</span>\n <span class=\"dt-cat-count\">{{ cat.names.length }}</span>\n </button>\n\n @if (!isCollapsed(cat.id)) {\n <div class=\"dt-grid\" role=\"listbox\" [attr.aria-label]=\"cat.label\">\n @for (name of cat.names; track name) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"dt-cell\"\n [class.is-selected]=\"selected() === name\"\n [attr.aria-selected]=\"selected() === name\"\n [title]=\"name\"\n (click)=\"select(name)\"\n >\n <fold-icon [name]=\"name\" size=\"md\" aria-hidden=\"true\" />\n </button>\n }\n </div>\n }\n </section>\n } @empty {\n <p class=\"dt-empty\">No icon matches \u201C{{ query() }}\u201D.</p>\n }\n </div>\n\n @if (selected(); as name) {\n <div class=\"dt-play\">\n <div class=\"dt-preview\">\n <fold-icon [name]=\"name\" [size]=\"size()\" />\n <code class=\"dt-name\">{{ name }}</code>\n </div>\n <div class=\"dt-controls\">\n <div class=\"dt-sizes\" role=\"group\" aria-label=\"Preview size\">\n @for (s of sizeSteps; track s) {\n <button\n type=\"button\"\n class=\"dt-size\"\n [class.is-on]=\"size() === s\"\n (click)=\"setSize(s)\"\n >\n {{ s }}\n </button>\n }\n </div>\n <div class=\"dt-snippet\">\n <code>{{ snippet() }}</code>\n <button\n type=\"button\"\n class=\"dt-copy\"\n [class.is-copied]=\"copied()\"\n (click)=\"copy()\"\n >\n <fold-icon\n [name]=\"copied() ? 'check' : 'copy'\"\n size=\"sm\"\n aria-hidden=\"true\"\n />\n {{ copied() ? \"Copied\" : \"Copy\" }}\n </button>\n </div>\n </div>\n </div>\n } @else {\n <p class=\"dt-hint\">\n Pick an icon to build a <code>&lt;fold-icon&gt;</code> snippet.\n </p>\n }\n </div>\n}\n", styles: [":host{position:fixed;z-index:2147483000;display:flex;flex-direction:column;width:320px;max-width:calc(100vw - 2rem);max-height:min(78vh,640px);overflow:hidden;background:var(--fold-color-surface-card);color:var(--fold-color-text);border:1px solid var(--fold-color-border);border-radius:var(--fold-radius-lg, 14px);box-shadow:var(--fold-shadow-lg);font-size:var(--fold-text-sm, .875rem);-webkit-user-select:none;user-select:none}:host(.docked){right:1.25rem;bottom:1.25rem}:host(.minimized){max-height:none}.dt-head{display:flex;align-items:center;gap:var(--fold-space-sm, .5rem);padding:.4rem .5rem .4rem .75rem;background:var(--fold-color-surface-raised);border-bottom:1px solid var(--fold-color-border-subtle);cursor:grab;touch-action:none}.dt-head:active{cursor:grabbing}:host(.minimized) .dt-head{border-bottom:0}.dt-grip{width:10px;height:14px;flex:none;background-image:radial-gradient(currentColor 1px,transparent 1.4px);background-size:4px 4px;color:var(--fold-color-text-faded);opacity:.7}.dt-title{display:flex;align-items:center;gap:.4rem;font-weight:600}.dt-count{padding:.05rem .4rem;border-radius:var(--fold-radius-pill, 9999px);background:var(--fold-color-surface-sunken);color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem);font-variant-numeric:tabular-nums}.dt-actions{display:flex;align-items:center;gap:.15rem;margin-left:auto}.dt-body{display:flex;flex-direction:column;gap:var(--fold-space-sm, .6rem);padding:var(--fold-space-sm, .6rem);min-height:0;overflow:hidden}.dt-search{position:relative;flex:none}.dt-search-ico{position:absolute;top:50%;left:.55rem;transform:translateY(-50%);color:var(--fold-color-text-muted);pointer-events:none}.dt-search-input{width:100%;box-sizing:border-box;padding:.4rem .6rem .4rem 1.9rem;font:inherit;color:var(--fold-color-text);background:var(--fold-color-surface-sunken);border:1px solid var(--fold-color-border-subtle);border-radius:var(--fold-radius-sm, 8px)}.dt-search-input::placeholder{color:var(--fold-color-text-faded)}.dt-search-input:focus-visible{outline:none;border-color:var(--fold-color-primary);box-shadow:0 0 0 2px var(--fold-color-primary-border)}.dt-cats{display:flex;flex-direction:column;gap:.15rem;overflow-y:auto;min-height:4rem;padding:.1rem}.dt-cat{display:flex;flex-direction:column}.dt-cat-head{all:unset;position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:.35rem;padding:.3rem .35rem;background:var(--fold-color-surface-card);color:var(--fold-color-text-muted);cursor:pointer}.dt-cat-head:hover{color:var(--fold-color-text)}.dt-cat-head:focus-visible{outline:2px solid var(--fold-color-primary-border);outline-offset:-2px;border-radius:var(--fold-radius-sm, 8px)}.dt-cat-label{font-size:var(--fold-text-xs, .75rem);font-weight:600;text-transform:uppercase;letter-spacing:.04em}.dt-cat-count{padding:.02rem .35rem;border-radius:var(--fold-radius-pill, 9999px);background:var(--fold-color-surface-sunken);color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem);font-variant-numeric:tabular-nums}.dt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(40px,1fr));gap:.25rem;padding:.1rem .1rem .35rem}.dt-cell{all:unset;display:grid;place-items:center;aspect-ratio:1;border-radius:var(--fold-radius-sm, 8px);color:var(--fold-color-text-secondary);cursor:pointer;transition:background var(--fold-motion-fast)}.dt-cell:hover{background:var(--fold-color-surface-hover);color:var(--fold-color-text)}.dt-cell:focus-visible{outline:2px solid var(--fold-color-primary-border);outline-offset:-2px}.dt-cell.is-selected{background:var(--fold-color-primary-surface, var(--fold-color-surface-hover));color:var(--fold-color-primary);box-shadow:inset 0 0 0 1px var(--fold-color-primary-border)}.dt-empty,.dt-hint{margin:0;padding:.5rem .25rem;color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem)}.dt-play{flex:none;display:flex;flex-direction:column;gap:.5rem;padding-top:.5rem;border-top:1px solid var(--fold-color-border-subtle)}.dt-preview{display:flex;align-items:center;gap:.6rem;color:var(--fold-color-text)}.dt-name{font-family:ui-monospace,monospace;font-size:var(--fold-text-xs, .75rem);color:var(--fold-color-text-muted)}.dt-controls{display:flex;flex-direction:column;gap:.4rem}.dt-sizes{display:inline-flex;gap:.15rem;padding:.15rem;background:var(--fold-color-surface-sunken);border-radius:var(--fold-radius-sm, 8px);width:fit-content}.dt-size{all:unset;padding:.15rem .45rem;border-radius:var(--fold-radius-xs, 5px);color:var(--fold-color-text-muted);font-size:var(--fold-text-xs, .75rem);cursor:pointer}.dt-size:hover{color:var(--fold-color-text)}.dt-size.is-on{background:var(--fold-color-surface-card);color:var(--fold-color-text);box-shadow:var(--fold-shadow-sm)}.dt-size:focus-visible{outline:2px solid var(--fold-color-primary-border)}.dt-snippet{display:flex;align-items:center;gap:.4rem}.dt-snippet code{flex:1 1 auto;min-width:0;overflow-x:auto;white-space:nowrap;padding:.35rem .5rem;background:var(--fold-color-surface-sunken);border-radius:var(--fold-radius-sm, 8px);font-family:ui-monospace,monospace;font-size:var(--fold-text-xs, .75rem);color:var(--fold-color-text)}.dt-copy{all:unset;display:inline-flex;align-items:center;gap:.3rem;flex:none;padding:.35rem .6rem;border-radius:var(--fold-radius-sm, 8px);background:var(--fold-color-primary);color:var(--fold-color-on-primary);font-size:var(--fold-text-xs, .75rem);font-weight:600;cursor:pointer}.dt-copy:hover{background:var(--fold-color-primary-strong, var(--fold-color-primary))}.dt-copy:focus-visible{outline:2px solid var(--fold-color-primary-border);outline-offset:2px}.dt-copy.is-copied{background:var(--fold-color-success, var(--fold-color-primary))}@media(prefers-reduced-motion:reduce){.dt-cell{transition:none}}\n"] }]
204
+ }], propDecorators: { closed: [{ type: i0.Output, args: ["closed"] }] } });
205
+
206
+ // Opt-in dev-tools entry — `import { … } from "fold-ng/devtools"`.
207
+ //
208
+ // Everything here is meant for a DEV build only. Reach the import behind a dev
209
+ // guard (`if (isDevMode())` + a dynamic `import("fold-ng/devtools")`, or a
210
+ // dev-only route/flag) so a production bundle tree-shakes it away — these tools
211
+ // are never part of the shipped UI.
212
+
213
+ /**
214
+ * Generated bundle index. Do not edit.
215
+ */
216
+
217
+ export { FoldIconDevtoolComponent };
218
+ //# sourceMappingURL=fold-ng-src-devtools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fold-ng-src-devtools.mjs","sources":["../../src/devtools/icon-devtool/icon-devtool.component.ts","../../src/devtools/icon-devtool/icon-devtool.component.html","../../src/devtools/public-api.ts","../../src/devtools/fold-ng-src-devtools.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n inject,\n output,\n signal,\n} from \"@angular/core\";\n// Cross-entry imports resolve through the package name, not a relative path\n// into `../../components`: `fold-ng/devtools` is its own ng-packagr entry point\n// (own rootDir), so it depends on the primary `fold-ng` entry the same way any\n// consumer would. The monorepo's vite/vitest alias `fold-ng` → `src/public-api`.\nimport {\n FOLD_BUILTIN_ICON_CATEGORIES,\n FoldButtonIconComponent,\n FoldIconComponent,\n FoldIconRegistry,\n} from \"fold-ng\";\n\n/** A draggable panel position, in viewport pixels (top-left origin). `null`\n * until the first drag — the panel sits in its default corner via CSS. */\ninterface DevtoolPos {\n readonly x: number;\n readonly y: number;\n}\n\nconst SIZE_STEPS = [\"xs\", \"sm\", \"md\", \"lg\", \"xl\"] as const;\ntype IconSize = (typeof SIZE_STEPS)[number];\n\n/** Bucket id for registry icons that belong to no built-in category. */\nconst CUSTOM_CATEGORY_ID = \"custom\";\n\n/** A category section rendered in the grid: its label, count, and members. */\ninterface IconCategory {\n readonly id: string;\n readonly label: string;\n readonly names: readonly string[];\n}\n\n/** name → built-in category id, for bucketing the live registry by theme. */\nconst CATEGORY_OF_NAME = new Map<string, string>(\n FOLD_BUILTIN_ICON_CATEGORIES.flatMap((category) =>\n category.names.map((name) => [name, category.id] as const),\n ),\n);\n\n/**\n * `<fold-icon-devtool>` — a **dev-only** floating panel that browses the live\n * {@link FoldIconRegistry} (built-ins + whatever the host app has registered),\n * with a search, a preview grid, and a mini playground that builds a\n * `<fold-icon>` snippet and copies it to the clipboard. The panel is **draggable**\n * (grab the header) and **minimisable** (collapses to a pill).\n *\n * **Ship it behind a dev guard.** It is published from the opt-in `fold-ng/devtools`\n * entry so it never lands in a bundle that doesn't ask for it; import it only on a\n * dev path so a production build tree-shakes it away:\n *\n * ```ts\n * import { isDevMode, signal } from \"@angular/core\";\n * protected readonly showIconTool = signal(false);\n * // template: @if (showIconTool()) { <fold-icon-devtool (closed)=\"showIconTool.set(false)\" /> }\n * // …and only reach the import behind `if (isDevMode())`.\n * ```\n *\n * @selector `fold-icon-devtool`\n */\n@Component({\n selector: \"fold-icon-devtool\",\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [FoldIconComponent, FoldButtonIconComponent],\n templateUrl: \"./icon-devtool.component.html\",\n styleUrl: \"./icon-devtool.component.scss\",\n host: {\n role: \"dialog\",\n \"aria-label\": \"Icon devtool\",\n \"[class.minimized]\": \"minimized()\",\n \"[style.left.px]\": \"pos()?.x ?? null\",\n \"[style.top.px]\": \"pos()?.y ?? null\",\n \"[class.docked]\": \"pos() === null\",\n },\n})\nexport class FoldIconDevtoolComponent {\n private readonly registry = inject(FoldIconRegistry);\n\n /** Emitted when the user closes the panel — the host should stop rendering it. */\n readonly closed = output();\n\n protected readonly sizeSteps = SIZE_STEPS;\n\n /** Collapsed to a pill (header only). */\n protected readonly minimized = signal(false);\n /** Free-drag position; `null` = docked in the default corner (CSS). */\n protected readonly pos = signal<DevtoolPos | null>(null);\n\n /** The live registry names (built-ins + host-registered), sorted. */\n private readonly allNames = computed(() => this.registry.names());\n\n protected readonly query = signal(\"\");\n /** Names matching the search (case-insensitive substring). */\n protected readonly matches = computed(() => {\n const q = this.query().trim().toLowerCase();\n const names = this.allNames();\n return q ? names.filter((n) => n.includes(q)) : names;\n });\n\n /** Category ids the user has collapsed (folded to their header). */\n private readonly collapsed = signal<ReadonlySet<string>>(new Set());\n\n /**\n * The matches grouped into category sections, in built-in order, with a\n * trailing **Custom** bucket for host-registered icons. Empty categories are\n * dropped so a search only shows the themes it actually hit.\n */\n protected readonly categories = computed<readonly IconCategory[]>(() => {\n const buckets = new Map<string, string[]>();\n for (const name of this.matches()) {\n const id = CATEGORY_OF_NAME.get(name) ?? CUSTOM_CATEGORY_ID;\n const bucket = buckets.get(id);\n if (bucket) {\n bucket.push(name);\n } else {\n buckets.set(id, [name]);\n }\n }\n const sections: IconCategory[] = [];\n for (const category of FOLD_BUILTIN_ICON_CATEGORIES) {\n const names = buckets.get(category.id);\n if (names) {\n sections.push({ id: category.id, label: category.label, names });\n }\n }\n const custom = buckets.get(CUSTOM_CATEGORY_ID);\n if (custom) {\n sections.push({ id: CUSTOM_CATEGORY_ID, label: \"Custom\", names: custom });\n }\n return sections;\n });\n\n /** Whether a category section is folded to its header. */\n protected isCollapsed(id: string): boolean {\n return this.collapsed().has(id);\n }\n\n protected toggleCategory(id: string): void {\n this.collapsed.update((set) => {\n const next = new Set(set);\n if (!next.delete(id)) {\n next.add(id);\n }\n return next;\n });\n }\n\n /** The icon selected into the playground (empty = none). */\n protected readonly selected = signal(\"\");\n /** Playground size preset. */\n protected readonly size = signal<IconSize>(\"md\");\n\n /** The snippet the copy button writes — size only emitted when non-default. */\n protected readonly snippet = computed(() => {\n const name = this.selected();\n if (!name) {\n return \"\";\n }\n const size = this.size();\n return size === \"md\"\n ? `<fold-icon name=\"${name}\" />`\n : `<fold-icon name=\"${name}\" size=\"${size}\" />`;\n });\n\n /** Flashes true briefly after a successful copy. */\n protected readonly copied = signal(false);\n\n protected select(name: string): void {\n this.selected.set(name);\n }\n\n protected setSize(size: IconSize): void {\n this.size.set(size);\n }\n\n protected onSearch(event: Event): void {\n const target = event.target;\n if (target instanceof HTMLInputElement) {\n this.query.set(target.value);\n }\n }\n\n protected copy(): void {\n const snippet = this.snippet();\n if (!snippet) {\n return;\n }\n void navigator.clipboard.writeText(snippet).then(() => {\n this.copied.set(true);\n setTimeout(() => this.copied.set(false), 1200);\n });\n }\n\n protected toggleMinimize(): void {\n this.minimized.update((m) => !m);\n }\n\n protected close(): void {\n this.closed.emit();\n }\n\n // ── Drag (grab the header) ────────────────────────────────────────────\n private dragDx = 0;\n private dragDy = 0;\n\n protected onDragStart(event: PointerEvent): void {\n const target = event.target;\n const handle = event.currentTarget;\n if (!(target instanceof HTMLElement) || !(handle instanceof HTMLElement)) {\n return;\n }\n // Ignore drags that start on an interactive header control.\n if (target.closest(\"button\")) {\n return;\n }\n const host = handle.closest(\"fold-icon-devtool\");\n if (!host) {\n return;\n }\n const rect = host.getBoundingClientRect();\n this.dragDx = event.clientX - rect.left;\n this.dragDy = event.clientY - rect.top;\n handle.setPointerCapture(event.pointerId);\n event.preventDefault();\n }\n\n protected onDragMove(event: PointerEvent): void {\n const handle = event.currentTarget;\n if (\n !(handle instanceof HTMLElement) ||\n !handle.hasPointerCapture(event.pointerId)\n ) {\n return;\n }\n const width = window.innerWidth;\n const height = window.innerHeight;\n const x = Math.max(0, Math.min(event.clientX - this.dragDx, width - 40));\n const y = Math.max(0, Math.min(event.clientY - this.dragDy, height - 40));\n this.pos.set({ x, y });\n }\n\n protected onDragEnd(event: PointerEvent): void {\n const handle = event.currentTarget;\n if (\n handle instanceof HTMLElement &&\n handle.hasPointerCapture(event.pointerId)\n ) {\n handle.releasePointerCapture(event.pointerId);\n }\n }\n}\n","<header\n class=\"dt-head\"\n (pointerdown)=\"onDragStart($event)\"\n (pointermove)=\"onDragMove($event)\"\n (pointerup)=\"onDragEnd($event)\"\n (pointercancel)=\"onDragEnd($event)\"\n>\n <span class=\"dt-grip\" aria-hidden=\"true\"></span>\n <span class=\"dt-title\">\n <fold-icon name=\"grid\" size=\"sm\" aria-hidden=\"true\" />\n Icons\n <span class=\"dt-count\">{{ matches().length }}</span>\n </span>\n <span class=\"dt-actions\">\n <fold-button-icon\n [icon]=\"minimized() ? 'chevron-up' : 'minus'\"\n size=\"sm\"\n [tooltip]=\"minimized() ? 'Expand' : 'Minimise'\"\n (clicked)=\"toggleMinimize()\"\n />\n <fold-button-icon\n icon=\"close\"\n size=\"sm\"\n tooltip=\"Close\"\n (clicked)=\"close()\"\n />\n </span>\n</header>\n\n@if (!minimized()) {\n <div class=\"dt-body\">\n <div class=\"dt-search\">\n <fold-icon\n name=\"search\"\n size=\"sm\"\n class=\"dt-search-ico\"\n aria-hidden=\"true\"\n />\n <input\n type=\"search\"\n class=\"dt-search-input\"\n placeholder=\"Search icons…\"\n aria-label=\"Search icons\"\n [value]=\"query()\"\n (input)=\"onSearch($event)\"\n />\n </div>\n\n <div class=\"dt-cats\">\n @for (cat of categories(); track cat.id) {\n <section class=\"dt-cat\">\n <button\n type=\"button\"\n class=\"dt-cat-head\"\n [attr.aria-expanded]=\"!isCollapsed(cat.id)\"\n (click)=\"toggleCategory(cat.id)\"\n >\n <fold-icon\n [name]=\"isCollapsed(cat.id) ? 'chevron-right' : 'chevron-down'\"\n size=\"sm\"\n aria-hidden=\"true\"\n />\n <span class=\"dt-cat-label\">{{ cat.label }}</span>\n <span class=\"dt-cat-count\">{{ cat.names.length }}</span>\n </button>\n\n @if (!isCollapsed(cat.id)) {\n <div class=\"dt-grid\" role=\"listbox\" [attr.aria-label]=\"cat.label\">\n @for (name of cat.names; track name) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"dt-cell\"\n [class.is-selected]=\"selected() === name\"\n [attr.aria-selected]=\"selected() === name\"\n [title]=\"name\"\n (click)=\"select(name)\"\n >\n <fold-icon [name]=\"name\" size=\"md\" aria-hidden=\"true\" />\n </button>\n }\n </div>\n }\n </section>\n } @empty {\n <p class=\"dt-empty\">No icon matches “{{ query() }}”.</p>\n }\n </div>\n\n @if (selected(); as name) {\n <div class=\"dt-play\">\n <div class=\"dt-preview\">\n <fold-icon [name]=\"name\" [size]=\"size()\" />\n <code class=\"dt-name\">{{ name }}</code>\n </div>\n <div class=\"dt-controls\">\n <div class=\"dt-sizes\" role=\"group\" aria-label=\"Preview size\">\n @for (s of sizeSteps; track s) {\n <button\n type=\"button\"\n class=\"dt-size\"\n [class.is-on]=\"size() === s\"\n (click)=\"setSize(s)\"\n >\n {{ s }}\n </button>\n }\n </div>\n <div class=\"dt-snippet\">\n <code>{{ snippet() }}</code>\n <button\n type=\"button\"\n class=\"dt-copy\"\n [class.is-copied]=\"copied()\"\n (click)=\"copy()\"\n >\n <fold-icon\n [name]=\"copied() ? 'check' : 'copy'\"\n size=\"sm\"\n aria-hidden=\"true\"\n />\n {{ copied() ? \"Copied\" : \"Copy\" }}\n </button>\n </div>\n </div>\n </div>\n } @else {\n <p class=\"dt-hint\">\n Pick an icon to build a <code>&lt;fold-icon&gt;</code> snippet.\n </p>\n }\n </div>\n}\n","// Opt-in dev-tools entry — `import { … } from \"fold-ng/devtools\"`.\n//\n// Everything here is meant for a DEV build only. Reach the import behind a dev\n// guard (`if (isDevMode())` + a dynamic `import(\"fold-ng/devtools\")`, or a\n// dev-only route/flag) so a production bundle tree-shakes it away — these tools\n// are never part of the shipped UI.\nexport { FoldIconDevtoolComponent } from \"./icon-devtool/icon-devtool.component\";\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}