@close-by/clay 0.2.5 → 0.2.7

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": "@close-by/clay",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "NiceGUI-like ui.* facade for Clay apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,10 +27,10 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@close-by/clay-compiler": "0.2.5",
31
- "@close-by/clay-core": "0.2.5",
32
- "@close-by/clay-components": "0.2.5",
33
- "@close-by/clay-server": "0.2.5",
30
+ "@close-by/clay-compiler": "0.2.7",
31
+ "@close-by/clay-core": "0.2.7",
32
+ "@close-by/clay-components": "0.2.7",
33
+ "@close-by/clay-server": "0.2.7",
34
34
  "@tailwindcss/cli": "^4.3.3",
35
35
  "tailwindcss": "^4.3.3"
36
36
  }
package/src/index.ts CHANGED
@@ -27,6 +27,8 @@ import {
27
27
  icon as iconFactory,
28
28
  iconText as iconTextFactory,
29
29
  statusDot as statusDotFactory,
30
+ feedRow as feedRowFactory,
31
+ feedList as feedListFactory,
30
32
  stat as statFactory,
31
33
  dataTable as dataTableFactory,
32
34
  areaChart as areaChartFactory,
@@ -129,6 +131,12 @@ import {
129
131
  type IconSlotProps,
130
132
  type StatusDotProps,
131
133
  type StatusDotColor,
134
+ type FeedRowProps,
135
+ type FeedRowStatus,
136
+ type FeedListProps,
137
+ type FeedRowProps,
138
+ type FeedRowStatus,
139
+ type FeedListProps,
132
140
  type StatItem,
133
141
  type DataTableProps,
134
142
  type DataTableAction,
@@ -841,6 +849,24 @@ export function statusDot(props?: StatusDotProps): Element {
841
849
  return statusDotFactory(props);
842
850
  }
843
851
 
852
+ /** Bordered container for live feed rows. See {@link FeedListProps}. */
853
+ export function feedList(props: FeedListProps, fn: () => void): Element;
854
+ export function feedList(fn: () => void): Element;
855
+ export function feedList(
856
+ propsOrFn: FeedListProps | (() => void),
857
+ fn?: () => void,
858
+ ): Element {
859
+ if (typeof propsOrFn === 'function') {
860
+ return feedListFactory(propsOrFn);
861
+ }
862
+ return feedListFactory(propsOrFn, fn!);
863
+ }
864
+
865
+ /** One live-feed row (status, link title, meta/issue, trailing time). See {@link FeedRowProps}. */
866
+ export function feedRow(props: FeedRowProps, footer?: () => void): Element {
867
+ return feedRowFactory(props, footer);
868
+ }
869
+
844
870
  /** Render markdown to HTML. Prefer `ui.markdown`. See {@link MarkdownProps}. */
845
871
  export function markdown(text?: string, props?: MarkdownProps): Element {
846
872
  return markdownFactory(text, props);
@@ -1594,6 +1620,10 @@ export const ui = {
1594
1620
  progress,
1595
1621
  separator,
1596
1622
  icon,
1623
+ iconText,
1624
+ statusDot,
1625
+ feedRow,
1626
+ feedList,
1597
1627
  markdown,
1598
1628
  html,
1599
1629
  image,
package/src/pages.ts CHANGED
@@ -58,6 +58,10 @@ export type PageMeta = {
58
58
  * UX only — still call `requireRole` in the page builder.
59
59
  */
60
60
  roles?: string[];
61
+ /** Nest this page under a collapsible sidebar group with the given label. */
62
+ group?: string;
63
+ /** Lucide icon for the group parent row (first page in the group with this set wins). */
64
+ groupIcon?: string;
61
65
  };
62
66
 
63
67
  const pageMetaByPath = new Map<string, PageMeta>();
@@ -140,8 +144,76 @@ export type NavFromPagesOptions = {
140
144
  role?: string;
141
145
  /** Viewer roles — page is shown if it has no `roles` meta, or any overlap. */
142
146
  roles?: string[];
147
+ /**
148
+ * When true, nest `/examples/*` routes (except `/examples/auth`) under an
149
+ * "Examples" group. Individual pages can override with `pageMeta.group`.
150
+ */
151
+ groupExamples?: boolean;
152
+ /** Label for the auto examples group. Default `"Examples"`. */
153
+ groupExamplesLabel?: string;
154
+ /** Icon for the auto examples group. Default `"layout-grid"`. */
155
+ groupExamplesIcon?: string;
143
156
  };
144
157
 
158
+ type RankedNavPage = {
159
+ href: string;
160
+ label: string;
161
+ icon: string;
162
+ order: number;
163
+ group?: string;
164
+ groupIcon?: string;
165
+ };
166
+
167
+ function resolvePageGroup(
168
+ href: string,
169
+ meta: PageMeta,
170
+ opts?: NavFromPagesOptions,
171
+ ): { group?: string; groupIcon?: string } {
172
+ if (meta.group !== undefined) {
173
+ return meta.group ? { group: meta.group, groupIcon: meta.groupIcon } : {};
174
+ }
175
+ if (opts?.groupExamples && href.startsWith('/examples/') && href !== '/examples/auth') {
176
+ return {
177
+ group: opts.groupExamplesLabel ?? 'Examples',
178
+ groupIcon: opts.groupExamplesIcon ?? 'layout-grid',
179
+ };
180
+ }
181
+ return {};
182
+ }
183
+
184
+ function buildNavFromRanked(ranked: RankedNavPage[]): AppNavItem[] {
185
+ const top: Array<{ order: number; node: AppNavItem }> = [];
186
+ const groups = new Map<string, AppNavItem>();
187
+
188
+ for (const row of ranked) {
189
+ const leaf: AppNavItem = { label: row.label, href: row.href, icon: row.icon };
190
+ if (!row.group) {
191
+ top.push({ order: row.order, node: leaf });
192
+ continue;
193
+ }
194
+
195
+ let parent = groups.get(row.group);
196
+ if (!parent) {
197
+ parent = {
198
+ label: row.group,
199
+ href: row.href,
200
+ icon: row.groupIcon ?? 'folder',
201
+ items: [],
202
+ };
203
+ groups.set(row.group, parent);
204
+ top.push({ order: row.order, node: parent });
205
+ } else {
206
+ const entry = top.find((t) => t.node === parent);
207
+ if (entry) entry.order = Math.min(entry.order, row.order);
208
+ if (row.groupIcon && parent.icon === 'folder') parent.icon = row.groupIcon;
209
+ }
210
+ parent.items!.push(leaf);
211
+ }
212
+
213
+ top.sort((a, b) => a.order - b.order || a.node.label.localeCompare(b.node.label));
214
+ return top.map((t) => t.node);
215
+ }
216
+
145
217
  /**
146
218
  * Build primary sidebar nav from registered pages + collected `pageMeta`.
147
219
  * Filters by `nav: false` and optional role overlap; sorts by `order` then path.
@@ -160,10 +232,11 @@ export function navFromPages(opts?: NavFromPagesOptions): AppNavItem[] {
160
232
  }
161
233
 
162
234
  const paths = getRegisteredPaths();
163
- const ranked = paths
235
+ const ranked: RankedNavPage[] = paths
164
236
  .map((href) => {
165
237
  const meta = pageMetaByPath.get(href) ?? {};
166
238
  const order = href === '/' ? 0 : (meta.order ?? 100);
239
+ const { group, groupIcon } = resolvePageGroup(href, meta, opts);
167
240
  return {
168
241
  href,
169
242
  label: meta.label ?? labelFromPath(href),
@@ -171,6 +244,8 @@ export function navFromPages(opts?: NavFromPagesOptions): AppNavItem[] {
171
244
  order,
172
245
  nav: meta.nav !== false,
173
246
  roles: meta.roles,
247
+ group,
248
+ groupIcon,
174
249
  };
175
250
  })
176
251
  .filter((item) => {
@@ -178,9 +253,10 @@ export function navFromPages(opts?: NavFromPagesOptions): AppNavItem[] {
178
253
  if (!item.roles || item.roles.length === 0) return true;
179
254
  if (viewer.size === 0) return false;
180
255
  return item.roles.some((r) => viewer.has(r));
181
- });
256
+ })
257
+ .map(({ nav: _nav, roles: _roles, ...rest }) => rest);
182
258
  ranked.sort((a, b) => a.order - b.order || a.href.localeCompare(b.href));
183
- return ranked.map(({ href, label, icon }) => ({ href, label, icon }));
259
+ return buildNavFromRanked(ranked);
184
260
  }
185
261
 
186
262
  /** Test helper: clear page registry + meta map. */