@frame-kit/ui-ng-patterns 0.1.18 → 0.1.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frame-kit/ui-ng-patterns",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Opinionated, dashboard-ready Angular component patterns composed from @frame-kit/ui-ng.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "@angular/forms": "^21.2.0",
18
18
  "@angular/platform-browser": "^21.2.0",
19
19
  "@angular/router": "^21.2.0",
20
- "@frame-kit/ui-ng": "0.1.18",
20
+ "@frame-kit/ui-ng": "0.1.20",
21
21
  "rxjs": "^7.0.0"
22
22
  },
23
23
  "sideEffects": false,
@@ -618,8 +618,8 @@ interface OnboardingSubstep {
618
618
  * - a **group** step — carries `substeps`, and is considered complete once every
619
619
  * substep is `done` (the top-level `done` flag is ignored for group steps).
620
620
  *
621
- * All state (`done`) is owned by the consumer; the widget is a controlled view
622
- * of the `steps` model and emits events when the user acts.
621
+ * All state (`done`, `pending`) is owned by the consumer; the widget is a
622
+ * controlled view of the `steps` model and emits events when the user acts.
623
623
  */
624
624
  interface OnboardingStep {
625
625
  /** Stable identifier, unique within the guide. */
@@ -628,10 +628,23 @@ interface OnboardingStep {
628
628
  label: string;
629
629
  /** Whether a leaf step is complete. Ignored when `substeps` is set. */
630
630
  done: boolean;
631
+ /**
632
+ * The step is in flight — not complete, but expected to complete without
633
+ * further user action (e.g. an invite that auto-completes when accepted).
634
+ * Purely visual: a pending step still counts as incomplete for progress.
635
+ */
636
+ pending?: boolean;
637
+ /** Optional supporting copy shown while the step is `pending`. */
638
+ pendingNote?: string;
631
639
  /** Optional supporting copy shown when the step is expanded. */
632
640
  description?: string;
633
641
  /** Optional call-to-action label (defaults to a generic "Start"). */
634
642
  actionLabel?: string;
643
+ /**
644
+ * Optional secondary text-action label (e.g. "Show help"). When set, the
645
+ * expanded body renders it and clicks emit `stepSecondaryAction`.
646
+ */
647
+ secondaryActionLabel?: string;
635
648
  /** When set, the step renders a sub-checklist and auto-completes when all are done. */
636
649
  substeps?: OnboardingSubstep[];
637
650
  }
@@ -648,19 +661,33 @@ interface Point {
648
661
  /**
649
662
  * A floating, draggable onboarding-progress widget. It presents a checklist of
650
663
  * steps with a progress bar, collapses to a compact pill with a progress ring,
651
- * and can be dragged anywhere on screen (CDK drag). It renders over the app in
652
- * a fixed layer — drop it once near the app root.
664
+ * and can be dragged around (CDK drag). It renders over the app in a fixed
665
+ * layer — drop it once near the app root.
653
666
  *
654
667
  * **Controlled model, no domain data.** The `steps` model is a required,
655
668
  * reactive input: the widget is a pure view of it and emits events when the
656
- * user acts (`stepAction`, `stepMarkDone`, `substepToggle`, `goLive`). The
657
- * consumer owns the step state — so if it is keyed by, say, the active account
658
- * or environment, swapping the input re-renders the widget instantly. The
659
- * widget never persists step state.
669
+ * user acts (`stepAction`, `stepMarkDone`, `stepSecondaryAction`,
670
+ * `substepToggle`, `footerAction`). The consumer owns the step state — so if
671
+ * it is keyed by, say, the active account or environment, swapping the input
672
+ * re-renders the widget instantly. The widget never persists step state.
673
+ *
674
+ * **Collapsed is a two-way model.** Bind `[(collapsed)]` (or listen to
675
+ * `collapsedChange`) to drive the card ↔ pill swap from outside — e.g. an
676
+ * auto-open policy. Left unbound, the widget manages it internally.
677
+ *
678
+ * **Container-aware.** `boundary` (a selector or element) confines both
679
+ * dragging and the widget's resting position to a region of the app — e.g. a
680
+ * dashboard's content area, so the widget can never cover navigation or a
681
+ * footer. The default resting spot is the boundary's bottom-right corner, and
682
+ * the widget re-clamps itself whenever its own footprint changes (pill ↔ card
683
+ * swap, a step expanding) or the viewport resizes — a corner-parked pill that
684
+ * expands into a large card pulls itself fully back into view. Until the user
685
+ * actually drags it, it stays anchored to the bottom-right corner.
660
686
  *
661
- * **Presentation state is internal + persisted.** Collapsed and drag position
662
- * live in the widget and persist to `localStorage` under `storageKey` — these
663
- * are UI preferences, not part of the model.
687
+ * **Presentation state is internal + optionally persisted.** Collapsed and
688
+ * drag position persist to `localStorage` under `storageKey`; pass
689
+ * `storageKey = null` for session-only behavior (e.g. when a consumer-side
690
+ * store owns presentation policy).
664
691
  *
665
692
  * **Opinionated but themeable.** It ships a house look out of the box; all
666
693
  * colour flows through `--fk-onboarding-guide-*` tokens, so overriding those
@@ -670,9 +697,12 @@ interface Point {
670
697
  * <fk-onboarding-guide
671
698
  * [steps]="steps()"
672
699
  * title="Setup guide"
700
+ * boundary=".app-content"
701
+ * [storageKey]="null"
702
+ * [(collapsed)]="collapsed"
673
703
  * (stepAction)="onStart($event)"
674
704
  * (substepToggle)="onToggle($event)"
675
- * (goLive)="onGoLive()"
705
+ * (footerAction)="onGoLive()"
676
706
  * (dismiss)="onDismiss()"
677
707
  * />
678
708
  * ```
@@ -685,16 +715,30 @@ declare class OnboardingGuideComponent {
685
715
  readonly steps: _angular_core.InputSignal<OnboardingStep[]>;
686
716
  /** Widget title. */
687
717
  readonly title: _angular_core.InputSignal<string>;
688
- /** `localStorage` key for the collapsed + drag-position preferences. */
689
- readonly storageKey: _angular_core.InputSignal<string>;
690
- /** Initial collapsed state, used only when nothing is persisted yet. */
691
- readonly startCollapsed: _angular_core.InputSignal<boolean>;
718
+ /**
719
+ * `localStorage` key for the collapsed + drag-position preferences.
720
+ * `null` disables persistence entirely — presentation state then lives and
721
+ * dies with the component instance (or with whatever the consumer binds).
722
+ */
723
+ readonly storageKey: _angular_core.InputSignal<string | null>;
724
+ /**
725
+ * Region the widget is confined to: a CSS selector or an element. Both
726
+ * dragging and the widget's resting position are clamped inside it. `null`
727
+ * (the default) confines to the viewport.
728
+ */
729
+ readonly boundary: _angular_core.InputSignal<string | HTMLElement | null>;
730
+ /** Breathing room kept between the widget and the boundary's edges. */
731
+ readonly boundaryMargin: _angular_core.InputSignal<number>;
732
+ /** Shows the rename (edit) affordance next to the title. */
733
+ readonly renamable: _angular_core.InputSignal<boolean>;
734
+ /** Shows the secondary "mark this step done" action on leaf steps. */
735
+ readonly showMarkDone: _angular_core.InputSignal<boolean>;
692
736
  /** Default CTA label when a step doesn't supply its own `actionLabel`. */
693
737
  readonly startLabel: _angular_core.InputSignal<string>;
694
738
  /** Secondary "mark this step done" action label. */
695
739
  readonly markDoneLabel: _angular_core.InputSignal<string>;
696
- /** Footer / final go-live action label. */
697
- readonly goLiveLabel: _angular_core.InputSignal<string>;
740
+ /** Footer action label (e.g. "Go live"). `null` hides the footer action. */
741
+ readonly footerActionLabel: _angular_core.InputSignal<string | null>;
698
742
  /** Footer noun for the "{done} of {total} {stepsWord} {doneWord}" summary. */
699
743
  readonly stepsWord: _angular_core.InputSignal<string>;
700
744
  /** Footer verb for the summary. */
@@ -715,34 +759,61 @@ declare class OnboardingGuideComponent {
715
759
  readonly dismissIcon: _angular_core.InputSignal<string>;
716
760
  readonly expandIcon: _angular_core.InputSignal<string>;
717
761
  readonly actionIcon: _angular_core.InputSignal<string>;
762
+ /**
763
+ * Whether the widget is collapsed to the pill. Two-way bindable so a
764
+ * consumer-side policy (auto-open on context switches, banner-triggered
765
+ * opens) can drive the card ↔ pill swap; unbound, the widget manages it.
766
+ */
767
+ readonly collapsed: _angular_core.ModelSignal<boolean>;
718
768
  /** A step's primary CTA was clicked. Emits the step id. */
719
769
  readonly stepAction: _angular_core.OutputEmitterRef<string>;
720
770
  /** A step's "mark done" was clicked. Emits the step id. */
721
771
  readonly stepMarkDone: _angular_core.OutputEmitterRef<string>;
772
+ /** A step's secondary text-action was clicked. Emits the step id. */
773
+ readonly stepSecondaryAction: _angular_core.OutputEmitterRef<string>;
722
774
  /** A substep checkbox was toggled. */
723
775
  readonly substepToggle: _angular_core.OutputEmitterRef<OnboardingSubstepToggle>;
724
- /** The final go-live action was invoked (footer or the go-live step CTA). */
725
- readonly goLive: _angular_core.OutputEmitterRef<void>;
776
+ /** The footer action was invoked. */
777
+ readonly footerAction: _angular_core.OutputEmitterRef<void>;
726
778
  /** The rename (edit) affordance was clicked; the consumer owns renaming. */
727
779
  readonly renameClick: _angular_core.OutputEmitterRef<void>;
728
780
  /** The dismiss control was clicked; the consumer decides whether to hide it. */
729
781
  readonly dismiss: _angular_core.OutputEmitterRef<void>;
730
- /** Fires when the collapsed state changes. */
731
- readonly collapsedChange: _angular_core.OutputEmitterRef<boolean>;
732
782
  readonly className: _angular_core.InputSignal<string>;
733
783
  readonly id: _angular_core.InputSignal<string | null>;
734
- protected readonly collapsed: _angular_core.WritableSignal<boolean>;
735
784
  protected readonly dragPosition: _angular_core.WritableSignal<Point>;
736
785
  /** True immediately after a real drag, so the drag-handle's click (e.g. the
737
786
  * pill's expand) is suppressed rather than firing at the drop point. */
738
787
  private justDragged;
739
788
  /**
740
- * The expanded step. A `linkedSignal` over `steps` so that whenever the model
741
- * changes (e.g. the consumer swaps in another account/environment's steps) it
742
- * resets to the first incomplete step; user toggles override until the next
743
- * model change.
789
+ * True once the user has deliberately placed the widget (a real drag now,
790
+ * or a persisted position from a previous session). Until then the widget
791
+ * re-anchors to the boundary's bottom-right corner on every viewport or
792
+ * footprint change instead of merely clamping its current spot.
744
793
  */
745
- protected readonly openStepId: _angular_core.WritableSignal<string | null>;
794
+ private userMoved;
795
+ private footprintObserver;
796
+ /** Guards the one-time preference restore in `hydrateOnce`. */
797
+ private hydrated;
798
+ /** True between CDK's drag start and drop — `reposition` stands down. */
799
+ private dragging;
800
+ /** The boundary element currently under observation (if any). */
801
+ private observedBoundary;
802
+ /**
803
+ * Last seen completion signature (step ids + done states) — the accordion
804
+ * reset skips the first run and cosmetic-only model updates.
805
+ */
806
+ private prevCompletionSignature;
807
+ /**
808
+ * The expanded step. Two-way bindable so a consumer can spotlight a
809
+ * specific step (e.g. a "complete this section" banner). `undefined` means
810
+ * "follow the default" — the first incomplete step. User toggles (and
811
+ * consumer writes) override the default until the `steps` model changes,
812
+ * which resets the choice so the guide re-focuses the next piece of work.
813
+ */
814
+ readonly openStepId: _angular_core.ModelSignal<string | null | undefined>;
815
+ /** The step actually expanded: the explicit choice, or the default. */
816
+ protected readonly resolvedOpenStepId: _angular_core.Signal<string | null>;
746
817
  protected readonly total: _angular_core.Signal<number>;
747
818
  protected readonly doneCount: _angular_core.Signal<number>;
748
819
  protected readonly percent: _angular_core.Signal<number>;
@@ -756,27 +827,67 @@ declare class OnboardingGuideComponent {
756
827
  get hostId(): string | null;
757
828
  constructor();
758
829
  isComplete(step: OnboardingStep): boolean;
830
+ isPending(step: OnboardingStep): boolean;
759
831
  protected isActive(step: OnboardingStep): boolean;
760
832
  protected actionLabelFor(step: OnboardingStep): string;
761
833
  protected toggleStep(stepId: string): void;
762
834
  protected onAction(step: OnboardingStep): void;
763
835
  protected onMarkDone(step: OnboardingStep): void;
836
+ protected onSecondaryAction(step: OnboardingStep): void;
764
837
  protected onSubstep(step: OnboardingStep, substep: OnboardingSubstep): void;
765
838
  protected onCollapse(): void;
766
839
  protected onExpand(): void;
767
840
  protected onRename(): void;
768
841
  protected onDismiss(): void;
769
- protected onGoLive(): void;
842
+ protected onFooterAction(): void;
770
843
  private setCollapsed;
771
844
  private onDragEnded;
845
+ /**
846
+ * Keep the resting position valid as the world changes around it. Two
847
+ * signals feed the same repositioning: the viewport resizing, and the
848
+ * widget's own footprint changing (pill ↔ card swap, a step expanding) —
849
+ * the latter is what stops a corner-parked pill from reopening as a card
850
+ * that's mostly off-screen.
851
+ */
852
+ private watchGeometry;
853
+ /**
854
+ * The boundary region can resize without a viewport resize — a side
855
+ * panel opening, chrome banners toggling — so the confining element is
856
+ * observed alongside the widget's own footprint. Re-synced whenever the
857
+ * `boundary` input changes.
858
+ */
859
+ private syncBoundaryObservation;
860
+ private resolveBoundaryElement;
861
+ private readonly onViewportResize;
862
+ private reposition;
863
+ /** The confining region: the `boundary` element's rect, or the viewport. */
864
+ private boundaryRect;
865
+ /**
866
+ * Constrain one axis to `[start + margin, end - size - margin]`.
867
+ *
868
+ * When the region can't fit the widget plus both margins that range is
869
+ * inverted, so the margin is dropped and the widget is fitted to the
870
+ * region's own edges instead — keeping it inside the boundary rather
871
+ * than letting the inset push it out. A widget genuinely larger than
872
+ * its region still overflows (the card has a fixed width and something
873
+ * has to give); it pins to the start edge.
874
+ */
875
+ private clampAxis;
772
876
  private clamp;
773
- private hydrate;
774
877
  private defaultBottomRight;
878
+ /**
879
+ * One-time restore of the persisted preferences, run from the first
880
+ * execution of the boundary effect — inputs are set by then, storage
881
+ * reads are synchronous, and it lands during initial change detection
882
+ * so the restored state is what the first paint shows. The caller's
883
+ * `reposition()` clamps the raw restored position right afterwards.
884
+ */
885
+ private hydrateOnce;
775
886
  private read;
776
887
  private persist;
777
888
  private storage;
778
889
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OnboardingGuideComponent, never>;
779
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<OnboardingGuideComponent, "fk-onboarding-guide", never, { "steps": { "alias": "steps"; "required": true; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "storageKey": { "alias": "storageKey"; "required": false; "isSignal": true; }; "startCollapsed": { "alias": "startCollapsed"; "required": false; "isSignal": true; }; "startLabel": { "alias": "startLabel"; "required": false; "isSignal": true; }; "markDoneLabel": { "alias": "markDoneLabel"; "required": false; "isSignal": true; }; "goLiveLabel": { "alias": "goLiveLabel"; "required": false; "isSignal": true; }; "stepsWord": { "alias": "stepsWord"; "required": false; "isSignal": true; }; "doneWord": { "alias": "doneWord"; "required": false; "isSignal": true; }; "stepsLeftWord": { "alias": "stepsLeftWord"; "required": false; "isSignal": true; }; "allDoneLabel": { "alias": "allDoneLabel"; "required": false; "isSignal": true; }; "renameLabel": { "alias": "renameLabel"; "required": false; "isSignal": true; }; "collapseLabel": { "alias": "collapseLabel"; "required": false; "isSignal": true; }; "dismissLabel": { "alias": "dismissLabel"; "required": false; "isSignal": true; }; "expandLabel": { "alias": "expandLabel"; "required": false; "isSignal": true; }; "checkIcon": { "alias": "checkIcon"; "required": false; "isSignal": true; }; "caretIcon": { "alias": "caretIcon"; "required": false; "isSignal": true; }; "editIcon": { "alias": "editIcon"; "required": false; "isSignal": true; }; "collapseIcon": { "alias": "collapseIcon"; "required": false; "isSignal": true; }; "dismissIcon": { "alias": "dismissIcon"; "required": false; "isSignal": true; }; "expandIcon": { "alias": "expandIcon"; "required": false; "isSignal": true; }; "actionIcon": { "alias": "actionIcon"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; }, { "stepAction": "stepAction"; "stepMarkDone": "stepMarkDone"; "substepToggle": "substepToggle"; "goLive": "goLive"; "renameClick": "renameClick"; "dismiss": "dismiss"; "collapsedChange": "collapsedChange"; }, never, never, true, [{ directive: typeof i1.CdkDrag; inputs: {}; outputs: {}; }]>;
890
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OnboardingGuideComponent, "fk-onboarding-guide", never, { "steps": { "alias": "steps"; "required": true; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "storageKey": { "alias": "storageKey"; "required": false; "isSignal": true; }; "boundary": { "alias": "boundary"; "required": false; "isSignal": true; }; "boundaryMargin": { "alias": "boundaryMargin"; "required": false; "isSignal": true; }; "renamable": { "alias": "renamable"; "required": false; "isSignal": true; }; "showMarkDone": { "alias": "showMarkDone"; "required": false; "isSignal": true; }; "startLabel": { "alias": "startLabel"; "required": false; "isSignal": true; }; "markDoneLabel": { "alias": "markDoneLabel"; "required": false; "isSignal": true; }; "footerActionLabel": { "alias": "footerActionLabel"; "required": false; "isSignal": true; }; "stepsWord": { "alias": "stepsWord"; "required": false; "isSignal": true; }; "doneWord": { "alias": "doneWord"; "required": false; "isSignal": true; }; "stepsLeftWord": { "alias": "stepsLeftWord"; "required": false; "isSignal": true; }; "allDoneLabel": { "alias": "allDoneLabel"; "required": false; "isSignal": true; }; "renameLabel": { "alias": "renameLabel"; "required": false; "isSignal": true; }; "collapseLabel": { "alias": "collapseLabel"; "required": false; "isSignal": true; }; "dismissLabel": { "alias": "dismissLabel"; "required": false; "isSignal": true; }; "expandLabel": { "alias": "expandLabel"; "required": false; "isSignal": true; }; "checkIcon": { "alias": "checkIcon"; "required": false; "isSignal": true; }; "caretIcon": { "alias": "caretIcon"; "required": false; "isSignal": true; }; "editIcon": { "alias": "editIcon"; "required": false; "isSignal": true; }; "collapseIcon": { "alias": "collapseIcon"; "required": false; "isSignal": true; }; "dismissIcon": { "alias": "dismissIcon"; "required": false; "isSignal": true; }; "expandIcon": { "alias": "expandIcon"; "required": false; "isSignal": true; }; "actionIcon": { "alias": "actionIcon"; "required": false; "isSignal": true; }; "collapsed": { "alias": "collapsed"; "required": false; "isSignal": true; }; "className": { "alias": "className"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "openStepId": { "alias": "openStepId"; "required": false; "isSignal": true; }; }, { "collapsed": "collapsedChange"; "stepAction": "stepAction"; "stepMarkDone": "stepMarkDone"; "stepSecondaryAction": "stepSecondaryAction"; "substepToggle": "substepToggle"; "footerAction": "footerAction"; "renameClick": "renameClick"; "dismiss": "dismiss"; "openStepId": "openStepIdChange"; }, never, never, true, [{ directive: typeof i1.CdkDrag; inputs: {}; outputs: {}; }]>;
780
891
  }
781
892
 
782
893
  /**