@frame-kit/ui-ng-patterns 0.1.19 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frame-kit/ui-ng-patterns",
3
- "version": "0.1.19",
3
+ "version": "0.2.0",
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.19",
20
+ "@frame-kit/ui-ng": "0.2.0",
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,41 @@ 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.
666
+ *
667
+ * **Presentational, not clever.** It renders exactly what it is given and
668
+ * reports what the user did — it derives no step state of its own. Which step
669
+ * is open (`openStepId`), which are done or pending (`steps`), whether it is
670
+ * collapsed (`collapsed`): all inputs. When a step should advance, when a
671
+ * context switch should re-focus the guide, when a choice has gone stale —
672
+ * all consumer decisions, because the consumer knows *what changed*, while
673
+ * the widget only ever sees a new array.
653
674
  *
654
675
  * **Controlled model, no domain data.** The `steps` model is a required,
655
676
  * 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.
677
+ * user acts (`stepAction`, `stepMarkDone`, `stepSecondaryAction`,
678
+ * `substepToggle`, `footerAction`). The consumer owns the step state — so if
679
+ * it is keyed by, say, the active account or environment, swapping the input
680
+ * re-renders the widget instantly. The widget never persists step state.
681
+ *
682
+ * **Collapsed and openStepId are two-way models.** Bind `[(collapsed)]` /
683
+ * `[(openStepId)]` to drive the card ↔ pill swap and the expanded row from
684
+ * outside; the widget writes them only in response to a direct user action.
660
685
  *
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.
686
+ * **Container-aware.** `boundary` (a selector or element) confines both
687
+ * dragging and the widget's resting position to a region of the app — e.g. a
688
+ * dashboard's content area, so the widget can never cover navigation or a
689
+ * footer. The default resting spot is the boundary's bottom-right corner, and
690
+ * the widget re-clamps itself whenever its own footprint changes (pill ↔ card
691
+ * swap, a step expanding) or the viewport resizes — a corner-parked pill that
692
+ * expands into a large card pulls itself fully back into view. Until the user
693
+ * actually drags it, it stays anchored to the bottom-right corner.
694
+ *
695
+ * **Presentation state is internal + optionally persisted.** Collapsed and
696
+ * drag position persist to `localStorage` under `storageKey`; pass
697
+ * `storageKey = null` for session-only behavior (e.g. when a consumer-side
698
+ * store owns presentation policy).
664
699
  *
665
700
  * **Opinionated but themeable.** It ships a house look out of the box; all
666
701
  * colour flows through `--fk-onboarding-guide-*` tokens, so overriding those
@@ -670,9 +705,12 @@ interface Point {
670
705
  * <fk-onboarding-guide
671
706
  * [steps]="steps()"
672
707
  * title="Setup guide"
708
+ * boundary=".app-content"
709
+ * [storageKey]="null"
710
+ * [(collapsed)]="collapsed"
673
711
  * (stepAction)="onStart($event)"
674
712
  * (substepToggle)="onToggle($event)"
675
- * (goLive)="onGoLive()"
713
+ * (footerAction)="onGoLive()"
676
714
  * (dismiss)="onDismiss()"
677
715
  * />
678
716
  * ```
@@ -685,16 +723,30 @@ declare class OnboardingGuideComponent {
685
723
  readonly steps: _angular_core.InputSignal<OnboardingStep[]>;
686
724
  /** Widget title. */
687
725
  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>;
726
+ /**
727
+ * `localStorage` key for the collapsed + drag-position preferences.
728
+ * `null` disables persistence entirely — presentation state then lives and
729
+ * dies with the component instance (or with whatever the consumer binds).
730
+ */
731
+ readonly storageKey: _angular_core.InputSignal<string | null>;
732
+ /**
733
+ * Region the widget is confined to: a CSS selector or an element. Both
734
+ * dragging and the widget's resting position are clamped inside it. `null`
735
+ * (the default) confines to the viewport.
736
+ */
737
+ readonly boundary: _angular_core.InputSignal<string | HTMLElement | null>;
738
+ /** Breathing room kept between the widget and the boundary's edges. */
739
+ readonly boundaryMargin: _angular_core.InputSignal<number>;
740
+ /** Shows the rename (edit) affordance next to the title. */
741
+ readonly renamable: _angular_core.InputSignal<boolean>;
742
+ /** Shows the secondary "mark this step done" action on leaf steps. */
743
+ readonly showMarkDone: _angular_core.InputSignal<boolean>;
692
744
  /** Default CTA label when a step doesn't supply its own `actionLabel`. */
693
745
  readonly startLabel: _angular_core.InputSignal<string>;
694
746
  /** Secondary "mark this step done" action label. */
695
747
  readonly markDoneLabel: _angular_core.InputSignal<string>;
696
- /** Footer / final go-live action label. */
697
- readonly goLiveLabel: _angular_core.InputSignal<string>;
748
+ /** Footer action label (e.g. "Go live"). `null` hides the footer action. */
749
+ readonly footerActionLabel: _angular_core.InputSignal<string | null>;
698
750
  /** Footer noun for the "{done} of {total} {stepsWord} {doneWord}" summary. */
699
751
  readonly stepsWord: _angular_core.InputSignal<string>;
700
752
  /** Footer verb for the summary. */
@@ -715,34 +767,55 @@ declare class OnboardingGuideComponent {
715
767
  readonly dismissIcon: _angular_core.InputSignal<string>;
716
768
  readonly expandIcon: _angular_core.InputSignal<string>;
717
769
  readonly actionIcon: _angular_core.InputSignal<string>;
770
+ /**
771
+ * Whether the widget is collapsed to the pill. Two-way bindable so a
772
+ * consumer-side policy (auto-open on context switches, banner-triggered
773
+ * opens) can drive the card ↔ pill swap; unbound, the widget manages it.
774
+ */
775
+ readonly collapsed: _angular_core.ModelSignal<boolean>;
718
776
  /** A step's primary CTA was clicked. Emits the step id. */
719
777
  readonly stepAction: _angular_core.OutputEmitterRef<string>;
720
778
  /** A step's "mark done" was clicked. Emits the step id. */
721
779
  readonly stepMarkDone: _angular_core.OutputEmitterRef<string>;
780
+ /** A step's secondary text-action was clicked. Emits the step id. */
781
+ readonly stepSecondaryAction: _angular_core.OutputEmitterRef<string>;
722
782
  /** A substep checkbox was toggled. */
723
783
  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>;
784
+ /** The footer action was invoked. */
785
+ readonly footerAction: _angular_core.OutputEmitterRef<void>;
726
786
  /** The rename (edit) affordance was clicked; the consumer owns renaming. */
727
787
  readonly renameClick: _angular_core.OutputEmitterRef<void>;
728
788
  /** The dismiss control was clicked; the consumer decides whether to hide it. */
729
789
  readonly dismiss: _angular_core.OutputEmitterRef<void>;
730
- /** Fires when the collapsed state changes. */
731
- readonly collapsedChange: _angular_core.OutputEmitterRef<boolean>;
732
790
  readonly className: _angular_core.InputSignal<string>;
733
791
  readonly id: _angular_core.InputSignal<string | null>;
734
- protected readonly collapsed: _angular_core.WritableSignal<boolean>;
735
792
  protected readonly dragPosition: _angular_core.WritableSignal<Point>;
736
793
  /** True immediately after a real drag, so the drag-handle's click (e.g. the
737
794
  * pill's expand) is suppressed rather than firing at the drop point. */
738
795
  private justDragged;
739
796
  /**
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.
797
+ * True once the user has deliberately placed the widget (a real drag now,
798
+ * or a persisted position from a previous session). Until then the widget
799
+ * re-anchors to the boundary's bottom-right corner on every viewport or
800
+ * footprint change instead of merely clamping its current spot.
801
+ */
802
+ private userMoved;
803
+ private footprintObserver;
804
+ /** Guards the one-time preference restore in `hydrateOnce`. */
805
+ private hydrated;
806
+ /** True between CDK's drag start and drop — `reposition` stands down. */
807
+ private dragging;
808
+ /** The boundary element currently under observation (if any). */
809
+ private observedBoundary;
810
+ /**
811
+ * The expanded step, or `null` for none. Fully controlled: the widget
812
+ * writes it only when the user toggles a row, and never infers a value
813
+ * of its own — which step should be open, when to advance to the next
814
+ * one, and whether a context switch should re-focus the guide are all
815
+ * decisions the consumer owns (it knows what changed; the widget only
816
+ * sees a new array).
744
817
  */
745
- protected readonly openStepId: _angular_core.WritableSignal<string | null>;
818
+ readonly openStepId: _angular_core.ModelSignal<string | null>;
746
819
  protected readonly total: _angular_core.Signal<number>;
747
820
  protected readonly doneCount: _angular_core.Signal<number>;
748
821
  protected readonly percent: _angular_core.Signal<number>;
@@ -756,27 +829,67 @@ declare class OnboardingGuideComponent {
756
829
  get hostId(): string | null;
757
830
  constructor();
758
831
  isComplete(step: OnboardingStep): boolean;
832
+ isPending(step: OnboardingStep): boolean;
759
833
  protected isActive(step: OnboardingStep): boolean;
760
834
  protected actionLabelFor(step: OnboardingStep): string;
761
835
  protected toggleStep(stepId: string): void;
762
836
  protected onAction(step: OnboardingStep): void;
763
837
  protected onMarkDone(step: OnboardingStep): void;
838
+ protected onSecondaryAction(step: OnboardingStep): void;
764
839
  protected onSubstep(step: OnboardingStep, substep: OnboardingSubstep): void;
765
840
  protected onCollapse(): void;
766
841
  protected onExpand(): void;
767
842
  protected onRename(): void;
768
843
  protected onDismiss(): void;
769
- protected onGoLive(): void;
844
+ protected onFooterAction(): void;
770
845
  private setCollapsed;
771
846
  private onDragEnded;
847
+ /**
848
+ * Keep the resting position valid as the world changes around it. Two
849
+ * signals feed the same repositioning: the viewport resizing, and the
850
+ * widget's own footprint changing (pill ↔ card swap, a step expanding) —
851
+ * the latter is what stops a corner-parked pill from reopening as a card
852
+ * that's mostly off-screen.
853
+ */
854
+ private watchGeometry;
855
+ /**
856
+ * The boundary region can resize without a viewport resize — a side
857
+ * panel opening, chrome banners toggling — so the confining element is
858
+ * observed alongside the widget's own footprint. Re-synced whenever the
859
+ * `boundary` input changes.
860
+ */
861
+ private syncBoundaryObservation;
862
+ private resolveBoundaryElement;
863
+ private readonly onViewportResize;
864
+ private reposition;
865
+ /** The confining region: the `boundary` element's rect, or the viewport. */
866
+ private boundaryRect;
867
+ /**
868
+ * Constrain one axis to `[start + margin, end - size - margin]`.
869
+ *
870
+ * When the region can't fit the widget plus both margins that range is
871
+ * inverted, so the margin is dropped and the widget is fitted to the
872
+ * region's own edges instead — keeping it inside the boundary rather
873
+ * than letting the inset push it out. A widget genuinely larger than
874
+ * its region still overflows (the card has a fixed width and something
875
+ * has to give); it pins to the start edge.
876
+ */
877
+ private clampAxis;
772
878
  private clamp;
773
- private hydrate;
774
879
  private defaultBottomRight;
880
+ /**
881
+ * One-time restore of the persisted preferences, run from the first
882
+ * execution of the boundary effect — inputs are set by then, storage
883
+ * reads are synchronous, and it lands during initial change detection
884
+ * so the restored state is what the first paint shows. The caller's
885
+ * `reposition()` clamps the raw restored position right afterwards.
886
+ */
887
+ private hydrateOnce;
775
888
  private read;
776
889
  private persist;
777
890
  private storage;
778
891
  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: {}; }]>;
892
+ 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
893
  }
781
894
 
782
895
  /**