@colixsystems/widget-sdk 0.196.0 → 0.197.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
@@ -77,7 +77,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
77
77
 
78
78
  ## Status
79
79
 
80
- `v0.196.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
80
+ `v0.197.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
81
+
82
+ ### What's new in 0.197.0 (contract 1.157.0)
83
+
84
+ **Host navigation: a page's sidebar menu can be opened from any page.** `resolveSidebarUnfold({ entries, active, sections })` used to answer only the section being viewed, so a host could draw a section's menu only while the visitor was inside it. The result now also carries `trees`: one `{ under, items, standalone, inside }` for every sidebar entry that carries a local menu, in menu order. `inside` is true for the section being viewed, which a host draws open; the others it draws folded behind the entry's arrow. `under`, `items` and `standalone` still describe the viewed section, so existing callers keep working. Widgets are unaffected: this is the host surface (`@colixsystems/widget-sdk/host`), and the widget contract version does not change.
81
85
 
82
86
  ### What's new in 0.196.0 (contract 1.157.0)
83
87
 
package/dist/host.d.ts CHANGED
@@ -254,14 +254,26 @@ export interface SidebarUnfoldItem {
254
254
  [field: string]: unknown;
255
255
  }
256
256
 
257
+ /** One section's local menu and the sidebar entry it hangs beneath. `inside`
258
+ * marks the section the visitor is in — the one that starts unfolded. */
259
+ export interface SidebarUnfoldTree {
260
+ under: string;
261
+ items: Array<SidebarUnfoldItem & { current: boolean }>;
262
+ standalone: boolean;
263
+ inside: boolean;
264
+ }
265
+
257
266
  /** Where a page's `SIDEBAR` local menu unfolds (REQ-NAV-LOCAL, sc-7696).
258
- * `under` is the global menu entry the items hang beneath, or null when
259
- * nothing unfolds. `standalone` marks the case where the global menu does not
260
- * list the page, so the page itself is the entry. */
267
+ * `under` is the global menu entry the visitor's own section hangs beneath,
268
+ * or null when they are in none. `standalone` marks the case where the global
269
+ * menu does not list the page, so the page itself is the entry. `trees` is
270
+ * every section an entry carries, in menu order, so an entry can offer its
271
+ * menu from any page; a `standalone` tree comes last. */
261
272
  export interface SidebarUnfold {
262
273
  under: string | null;
263
274
  items: Array<SidebarUnfoldItem & { current: boolean }>;
264
275
  standalone: boolean;
276
+ trees: SidebarUnfoldTree[];
265
277
  }
266
278
 
267
279
  /** Resolve where the viewed page's local menu unfolds in the sidebar and the
@@ -636,21 +636,25 @@ function resolveTopBarTokens(theme) {
636
636
  * inside it, so the menu is never lost — a section off the menu is exactly
637
637
  * the case that would otherwise render blank.
638
638
  *
639
+ * sc-10693: `trees` answers EVERY section, not only the visitor's own, so a
640
+ * section's entry can offer its menu from any page. `under` / `items` /
641
+ * `standalone` still name the one the visitor is inside.
642
+ *
639
643
  * @param {{ entries?: unknown, active?: unknown, sections?: unknown }} input
640
644
  * `entries` — the global menu's keys, in menu order.
641
645
  * `active` — the key of the page being viewed.
642
646
  * `sections` — `{ [key]: { menu, items } }` for pages whose local nav is
643
647
  * `SIDEBAR`; `menu` identifies the saved menu, `items` carry their own key.
644
- * @returns {{ under: string|null, items: Array<object>, standalone: boolean }}
645
- * `under` is null when nothing unfolds; when `standalone` it names the
646
- * section's own page, which the host draws as the entry. Each item is
647
- * returned with a `current` flag rather than leaving each host to
648
- * recompute it.
648
+ * @returns {{ under: string|null, items: Array<object>, standalone: boolean, trees: Array<object> }}
649
+ * `under` is null when the visitor is in no section; when `standalone` it
650
+ * names the section's own page, which the host draws as the entry. Each
651
+ * item is returned with a `current` flag rather than leaving each host to
652
+ * recompute it. `trees` lists `{ under, items, standalone, inside }` in menu
653
+ * order, a `standalone` one last; `inside` marks the visitor's own.
649
654
  */
650
655
  function resolveSidebarUnfold(input) {
651
656
  const { entries, active, sections } = isPlainObject(input) ? input : {};
652
- const nothing = { under: null, items: [], standalone: false };
653
- if (typeof active !== "string" || !active) return nothing;
657
+ const viewed = typeof active === "string" && active ? active : null;
654
658
  const bank = isPlainObject(sections) ? sections : {};
655
659
  const order = Array.isArray(entries) ? entries : [];
656
660
  const sectionAt = (key) => {
@@ -663,31 +667,60 @@ function resolveSidebarUnfold(input) {
663
667
  return (
664
668
  Boolean(candidate) &&
665
669
  candidate.items.some(
666
- (item) => isPlainObject(item) && item.key === active,
670
+ (item) => isPlainObject(item) && item.key === viewed,
667
671
  )
668
672
  );
669
673
  };
670
674
  // The page's own section wins; otherwise the section whose menu lists it,
671
675
  // preferring one a global entry carries so the tree hangs where it always
672
676
  // does. Both are the same section from the visitor's side.
673
- const section =
674
- sectionAt(active) ||
675
- sectionAt(order.find(lists)) ||
676
- sectionAt(Object.keys(bank).find(lists));
677
- if (!section) return nothing;
678
- const marked = section.items.map((item) => ({
679
- ...(isPlainObject(item) ? item : {}),
680
- current:
681
- isPlainObject(item) && typeof item.key === "string" && item.key === active,
682
- }));
683
- const owner = order.find((key) => {
684
- const candidate = isPlainObject(bank[key]) ? bank[key] : null;
685
- return Boolean(candidate) && candidate.menu === section.menu;
686
- });
687
- if (typeof owner === "string" && owner) {
688
- return { under: owner, items: marked, standalone: false };
677
+ const section = viewed
678
+ ? sectionAt(viewed) ||
679
+ sectionAt(order.find(lists)) ||
680
+ sectionAt(Object.keys(bank).find(lists))
681
+ : null;
682
+ const mark = (items) =>
683
+ items.map((item) => ({
684
+ ...(isPlainObject(item) ? item : {}),
685
+ current:
686
+ viewed !== null &&
687
+ isPlainObject(item) &&
688
+ typeof item.key === "string" &&
689
+ item.key === viewed,
690
+ }));
691
+ // A saved menu hangs under the FIRST global entry carrying it, so two
692
+ // entries sharing one menu never draw it twice.
693
+ const claimed = [];
694
+ const trees = [];
695
+ for (const key of order) {
696
+ if (!isPlainObject(bank[key])) continue;
697
+ const { menu } = bank[key];
698
+ if (claimed.includes(menu)) continue;
699
+ claimed.push(menu);
700
+ // An unusable key still owns its menu: nothing may hang it elsewhere.
701
+ if (typeof key !== "string" || !key) continue;
702
+ const inside = Boolean(section) && menu === section.menu;
703
+ const own = inside ? section : sectionAt(key);
704
+ if (!own) continue;
705
+ trees.push({ under: key, items: mark(own.items), standalone: false, inside });
706
+ }
707
+ if (!section) return { under: null, items: [], standalone: false, trees };
708
+ const owned = trees.find((tree) => tree.inside);
709
+ if (owned) {
710
+ return { under: owned.under, items: owned.items, standalone: false, trees };
689
711
  }
690
- return { under: section.key, items: marked, standalone: true };
712
+ const alone = {
713
+ under: section.key,
714
+ items: mark(section.items),
715
+ standalone: true,
716
+ inside: true,
717
+ };
718
+ return {
719
+ under: alone.under,
720
+ items: alone.items,
721
+ standalone: true,
722
+ trees: [...trees, alone],
723
+ };
691
724
  }
692
725
 
693
726
  /**
@@ -627,21 +627,25 @@ export function resolveTopBarTokens(theme) {
627
627
  * inside it, so the menu is never lost — a section off the menu is exactly
628
628
  * the case that would otherwise render blank.
629
629
  *
630
+ * sc-10693: `trees` answers EVERY section, not only the visitor's own, so a
631
+ * section's entry can offer its menu from any page. `under` / `items` /
632
+ * `standalone` still name the one the visitor is inside.
633
+ *
630
634
  * @param {{ entries?: unknown, active?: unknown, sections?: unknown }} input
631
635
  * `entries` — the global menu's keys, in menu order.
632
636
  * `active` — the key of the page being viewed.
633
637
  * `sections` — `{ [key]: { menu, items } }` for pages whose local nav is
634
638
  * `SIDEBAR`; `menu` identifies the saved menu, `items` carry their own key.
635
- * @returns {{ under: string|null, items: Array<object>, standalone: boolean }}
636
- * `under` is null when nothing unfolds; when `standalone` it names the
637
- * section's own page, which the host draws as the entry. Each item is
638
- * returned with a `current` flag rather than leaving each host to
639
- * recompute it.
639
+ * @returns {{ under: string|null, items: Array<object>, standalone: boolean, trees: Array<object> }}
640
+ * `under` is null when the visitor is in no section; when `standalone` it
641
+ * names the section's own page, which the host draws as the entry. Each
642
+ * item is returned with a `current` flag rather than leaving each host to
643
+ * recompute it. `trees` lists `{ under, items, standalone, inside }` in menu
644
+ * order, a `standalone` one last; `inside` marks the visitor's own.
640
645
  */
641
646
  export function resolveSidebarUnfold(input) {
642
647
  const { entries, active, sections } = isPlainObject(input) ? input : {};
643
- const nothing = { under: null, items: [], standalone: false };
644
- if (typeof active !== "string" || !active) return nothing;
648
+ const viewed = typeof active === "string" && active ? active : null;
645
649
  const bank = isPlainObject(sections) ? sections : {};
646
650
  const order = Array.isArray(entries) ? entries : [];
647
651
  const sectionAt = (key) => {
@@ -654,31 +658,60 @@ export function resolveSidebarUnfold(input) {
654
658
  return (
655
659
  Boolean(candidate) &&
656
660
  candidate.items.some(
657
- (item) => isPlainObject(item) && item.key === active,
661
+ (item) => isPlainObject(item) && item.key === viewed,
658
662
  )
659
663
  );
660
664
  };
661
665
  // The page's own section wins; otherwise the section whose menu lists it,
662
666
  // preferring one a global entry carries so the tree hangs where it always
663
667
  // does. Both are the same section from the visitor's side.
664
- const section =
665
- sectionAt(active) ||
666
- sectionAt(order.find(lists)) ||
667
- sectionAt(Object.keys(bank).find(lists));
668
- if (!section) return nothing;
669
- const marked = section.items.map((item) => ({
670
- ...(isPlainObject(item) ? item : {}),
671
- current:
672
- isPlainObject(item) && typeof item.key === "string" && item.key === active,
673
- }));
674
- const owner = order.find((key) => {
675
- const candidate = isPlainObject(bank[key]) ? bank[key] : null;
676
- return Boolean(candidate) && candidate.menu === section.menu;
677
- });
678
- if (typeof owner === "string" && owner) {
679
- return { under: owner, items: marked, standalone: false };
668
+ const section = viewed
669
+ ? sectionAt(viewed) ||
670
+ sectionAt(order.find(lists)) ||
671
+ sectionAt(Object.keys(bank).find(lists))
672
+ : null;
673
+ const mark = (items) =>
674
+ items.map((item) => ({
675
+ ...(isPlainObject(item) ? item : {}),
676
+ current:
677
+ viewed !== null &&
678
+ isPlainObject(item) &&
679
+ typeof item.key === "string" &&
680
+ item.key === viewed,
681
+ }));
682
+ // A saved menu hangs under the FIRST global entry carrying it, so two
683
+ // entries sharing one menu never draw it twice.
684
+ const claimed = [];
685
+ const trees = [];
686
+ for (const key of order) {
687
+ if (!isPlainObject(bank[key])) continue;
688
+ const { menu } = bank[key];
689
+ if (claimed.includes(menu)) continue;
690
+ claimed.push(menu);
691
+ // An unusable key still owns its menu: nothing may hang it elsewhere.
692
+ if (typeof key !== "string" || !key) continue;
693
+ const inside = Boolean(section) && menu === section.menu;
694
+ const own = inside ? section : sectionAt(key);
695
+ if (!own) continue;
696
+ trees.push({ under: key, items: mark(own.items), standalone: false, inside });
697
+ }
698
+ if (!section) return { under: null, items: [], standalone: false, trees };
699
+ const owned = trees.find((tree) => tree.inside);
700
+ if (owned) {
701
+ return { under: owned.under, items: owned.items, standalone: false, trees };
680
702
  }
681
- return { under: section.key, items: marked, standalone: true };
703
+ const alone = {
704
+ under: section.key,
705
+ items: mark(section.items),
706
+ standalone: true,
707
+ inside: true,
708
+ };
709
+ return {
710
+ under: alone.under,
711
+ items: alone.items,
712
+ standalone: true,
713
+ trees: [...trees, alone],
714
+ };
682
715
  }
683
716
 
684
717
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.196.0",
3
+ "version": "0.197.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",