@apliteni/apliteni-ui 0.49.1 → 0.50.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.
@@ -0,0 +1,53 @@
1
+ # Text length
2
+
3
+ ## Keep controls short
4
+
5
+ <!-- rule: short-controls -->
6
+
7
+ **Rule:** Labels, buttons, badges and status text name the field, action or state in a few words.
8
+
9
+ **Do:** “Save invoice.”
10
+
11
+ **Don't:** “Click here to save your invoice.”
12
+
13
+ ## Explain data in captions
14
+
15
+ <!-- rule: useful-captions -->
16
+
17
+ **Rule:** Use one sentence for needed units, dates or limits.
18
+
19
+ **Do:** “March revenue in EUR, excluding refunds.”
20
+
21
+ **Don't:** “An overview of your revenue.”
22
+
23
+ ## Use introductions only when needed
24
+
25
+ <!-- rule: useful-introductions -->
26
+
27
+ **Rule:** Add a short introduction only for a purpose or step the title and controls leave unclear.
28
+
29
+ **Do:** “Upload a CSV with invoice numbers, dates and amounts.”
30
+
31
+ **Don't:** “Manage your invoices here.”
32
+
33
+ ## Explain consequences in callouts
34
+
35
+ <!-- rule: useful-callouts -->
36
+
37
+ **Rule:** Explain a consequence or next step in one or two sentences.
38
+
39
+ **Do:** “Deleting this workspace deletes its reports. Export them first.”
40
+
41
+ **Don't:** “Think carefully before proceeding.”
42
+
43
+ **Except:** Keep needed safety, consent and legal details visible, however long. Link to optional background.
44
+
45
+ ## Cut filler
46
+
47
+ <!-- rule: remove-filler -->
48
+
49
+ **Rule:** Cut text readers can decide and finish without.
50
+
51
+ **Do:** “Exports include only filtered rows.”
52
+
53
+ **Don't:** “Export quickly and easily.”
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
package/react/README.md CHANGED
@@ -433,3 +433,20 @@ function SaveButton() {
433
433
 
434
434
  <Toast><SaveButton /></Toast>
435
435
  ```
436
+
437
+ ## AppShell
438
+
439
+ AppShell includes the kit’s rail and top band. Pass the current router pathname so the longest matching section path becomes active. Spread the props from `renderLink` onto your router link. The rail’s folded state is saved in a shared cookie. On phones, the bar shows up to four sections. If there are more, it shows three sections and a More sheet. Provide no more than one primary page action. The palette defaults to section links; pass `palette` to add custom commands. Import both the kit CSS and React CSS.
440
+
441
+ ```tsx
442
+ <AppShell
443
+ pathname={location.pathname}
444
+ sections={[{ href: '/reports', label: 'Reports', icon: 'chart' }]}
445
+ title="Reports"
446
+ account={{ name: 'Demo User', email: 'demo@example.com' }}
447
+ onSignOut={signOut}
448
+ renderLink={(section, { href, ...props }) => <Link to={section.href} {...props} />}
449
+ >
450
+ <Reports />
451
+ </AppShell>
452
+ ```
@@ -525,3 +525,71 @@
525
525
  .rx-toast-stack > .ui-toast {
526
526
  pointer-events: auto;
527
527
  }
528
+
529
+ /* src/AppShell.css */
530
+ .ui-react-app__bottom {
531
+ display: none;
532
+ }
533
+ .ui-react-app__actions {
534
+ display: flex;
535
+ flex-wrap: wrap;
536
+ gap: var(--space-3);
537
+ margin-bottom: var(--space-5);
538
+ }
539
+ .ui-react-app .ui-app__brand .ui-react-app__mark {
540
+ display: inline-flex;
541
+ opacity: 1;
542
+ }
543
+ .ui-react-app__bottom .ui-nav__item {
544
+ min-height: 48px;
545
+ }
546
+ @media (max-width: 560px) {
547
+ .ui-react-app {
548
+ display: block;
549
+ }
550
+ .ui-react-app > .ui-app__rail {
551
+ display: none;
552
+ }
553
+ .ui-react-app .ui-app__main {
554
+ padding-bottom: calc(96px + env(safe-area-inset-bottom));
555
+ }
556
+ .ui-react-app .ui-app__bar {
557
+ padding-inline: var(--space-4);
558
+ }
559
+ .ui-react-app .ui-app__search {
560
+ min-width: 0;
561
+ }
562
+ .ui-react-app .ui-app__search kbd {
563
+ display: none;
564
+ }
565
+ .ui-react-app__bottom {
566
+ display: block;
567
+ position: fixed;
568
+ inset: auto 0 0;
569
+ z-index: var(--z-sticky);
570
+ padding: var(--space-2) var(--space-2) calc(var(--space-2) + env(safe-area-inset-bottom));
571
+ background: var(--bg);
572
+ border-top: 1px solid var(--border);
573
+ }
574
+ .ui-react-app__bottom .ui-nav__list {
575
+ display: flex;
576
+ flex-direction: row;
577
+ }
578
+ .ui-react-app__bottom li {
579
+ flex: 1;
580
+ min-width: 0;
581
+ }
582
+ .ui-react-app__bottom .ui-nav__item {
583
+ justify-content: center;
584
+ flex-direction: column;
585
+ gap: var(--space-1);
586
+ padding-inline: var(--space-1);
587
+ }
588
+ .ui-react-app__bottom .ui-nav__label {
589
+ flex: none;
590
+ font-size: var(--text-xs);
591
+ }
592
+ .ui-react-app__bottom .ui-nav__badge {
593
+ display: none;
594
+ }
595
+ }
@@ -550,7 +550,43 @@ type ToastProps = {
550
550
  declare function useToast(): (notice: ToastNotice) => void;
551
551
  declare function Toast({ children }: ToastProps): react.JSX.Element;
552
552
 
553
+ type AppShellSection = {
554
+ href: string;
555
+ label: string;
556
+ icon: string;
557
+ count?: number;
558
+ };
559
+ type AppShellProps = {
560
+ sections: AppShellSection[];
561
+ /** Current router pathname; the longest matching section wins. */
562
+ pathname: string;
563
+ title: string;
564
+ children?: ReactNode;
565
+ word?: string;
566
+ brand?: ReactNode;
567
+ brandHref?: string;
568
+ width?: 'centered' | 'wide';
569
+ back?: {
570
+ href: string;
571
+ label: string;
572
+ };
573
+ lede?: string;
574
+ actions?: ReactNode;
575
+ bandControl?: ReactNode;
576
+ account: {
577
+ name: string;
578
+ email: string;
579
+ };
580
+ onSignOut: () => void;
581
+ /** Server-read fold preference. The browser restores the shared rail cookie. */
582
+ defaultCollapsed?: boolean;
583
+ palette?: Omit<CommandPaletteProps, 'open' | 'onClose'>;
584
+ /** Spread linkProps onto the router link, including children and onClick. */
585
+ renderLink?: (section: AppShellSection, linkProps: ComponentPropsWithoutRef<'a'>) => ReactNode;
586
+ };
587
+ declare function AppShell({ sections, pathname, title, children, word, brand, brandHref, width, back, lede, actions, bandControl, account, onSignOut, defaultCollapsed, palette, renderLink }: AppShellProps): react.JSX.Element;
588
+
553
589
  declare const PAGE_SIZES: readonly number[];
554
590
  declare const DEFAULT_PAGE_SIZE: number;
555
591
 
556
- export { BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, Timeline, type TimelineEvent, type TimelineProps, Toast, type ToastNotice, type ToastProps, sortTableRows, useToast };
592
+ export { AppShell, type AppShellProps, type AppShellSection, BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Callout, type CalloutProps, type CalloutVariant, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, Confirm, type ConfirmProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, DeltaValue, type DeltaValueProps, Denied, type DeniedProps, Drawer, type DrawerProps, DrawerSection, type DrawerSectionProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, FileField, type FileFieldProps, type Filter, FilterBar, type FilterBarProps, Icon, KeyValueList, type KeyValueListProps, type KeyValueRow, Modal, type ModalProps, NumericValue, type NumericValueProps, PAGE_SIZES, Pagination, type PaginationProps, RowIdentity, type RowIdentityProps, type SegmentOption, Segmented, type SegmentedProps, SelectField, type SelectFieldProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, Snippet, type SnippetProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TabItem, type TableSort, Tabs, type TabsProps, TextArea, type TextAreaProps, TextField, type TextFieldProps, Timeline, type TimelineEvent, type TimelineProps, Toast, type ToastNotice, type ToastProps, sortTableRows, useToast };
@@ -2152,10 +2152,171 @@ function Toast({ children }) {
2152
2152
  ] });
2153
2153
  }
2154
2154
 
2155
+ // src/AppShell.tsx
2156
+ import { useEffect as useEffect9, useId as useId9, useState as useState9 } from "react";
2157
+ import { applyTheme, paletteHotkey, prism, railCollapsed, themeIcon, themeName } from "@apliteni/apliteni-ui";
2158
+ import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
2159
+ var path = (value) => value.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
2160
+ function AppShell({
2161
+ sections,
2162
+ pathname,
2163
+ title,
2164
+ children,
2165
+ word = "apliteni-ui",
2166
+ brand,
2167
+ brandHref = "/",
2168
+ width = "centered",
2169
+ back,
2170
+ lede,
2171
+ actions,
2172
+ bandControl,
2173
+ account,
2174
+ onSignOut,
2175
+ defaultCollapsed = false,
2176
+ palette,
2177
+ renderLink
2178
+ }) {
2179
+ const uid = useId9();
2180
+ const [collapsed, setCollapsed] = useState9(defaultCollapsed);
2181
+ const [search, setSearch] = useState9(false);
2182
+ const [more, setMore] = useState9(false);
2183
+ const [theme, setTheme] = useState9("dark");
2184
+ const current = path(pathname);
2185
+ const active = sections.filter((section) => current === path(section.href) || path(section.href) !== "/" && current.startsWith(`${path(section.href)}/`)).sort((a, b) => path(b.href).length - path(a.href).length)[0];
2186
+ useEffect9(() => {
2187
+ setCollapsed(railCollapsed() ?? defaultCollapsed);
2188
+ }, [defaultCollapsed]);
2189
+ useEffect9(() => {
2190
+ setMore(false);
2191
+ setSearch(false);
2192
+ }, [pathname]);
2193
+ useEffect9(() => {
2194
+ const root = document.documentElement;
2195
+ const sync2 = () => setTheme(root.getAttribute("data-theme") === "light" ? "light" : "dark");
2196
+ sync2();
2197
+ const observer = new MutationObserver(sync2);
2198
+ observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
2199
+ return () => observer.disconnect();
2200
+ }, []);
2201
+ useEffect9(() => {
2202
+ const hotkey = (event) => {
2203
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "k") return;
2204
+ const target = event.target;
2205
+ if (target?.closest?.('input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="dialog"]')) return;
2206
+ event.preventDefault();
2207
+ setSearch(true);
2208
+ };
2209
+ document.addEventListener("keydown", hotkey);
2210
+ return () => document.removeEventListener("keydown", hotkey);
2211
+ }, []);
2212
+ const fold = () => {
2213
+ const next = !collapsed;
2214
+ setCollapsed(next);
2215
+ try {
2216
+ document.cookie = `apliteni-ui-rail=${next ? "collapsed" : "expanded"}; Max-Age=31536000; Path=/; SameSite=Lax`;
2217
+ } catch {
2218
+ }
2219
+ };
2220
+ const links = (items) => items.map((section) => {
2221
+ const linkProps = {
2222
+ href: section.href,
2223
+ className: `ui-nav__item${section === active ? " is-active" : ""}`,
2224
+ "aria-current": section === active ? "page" : void 0,
2225
+ "aria-label": `${section.label}${section.count == null ? "" : ` ${section.count}`}`,
2226
+ onClick: (event) => {
2227
+ if (!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) setMore(false);
2228
+ },
2229
+ children: /* @__PURE__ */ jsxs21(Fragment7, { children: [
2230
+ /* @__PURE__ */ jsx25("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx25(Icon, { name: section.icon }) }),
2231
+ /* @__PURE__ */ jsx25("span", { className: "ui-nav__label", children: section.label }),
2232
+ section.count != null && /* @__PURE__ */ jsx25("span", { className: "ui-nav__badge", children: section.count })
2233
+ ] })
2234
+ };
2235
+ return /* @__PURE__ */ jsx25("li", { children: renderLink ? renderLink(section, linkProps) : /* @__PURE__ */ jsx25("a", { ...linkProps }) }, section.href);
2236
+ });
2237
+ const foldLabel = collapsed ? "Expand sidebar" : "Collapse sidebar";
2238
+ const visible = sections.length > 4 ? sections.slice(0, 3) : sections;
2239
+ const remaining = sections.slice(visible.length);
2240
+ const initials = account.name.trim().split(/\s+/).map((part) => part[0]).slice(0, 2).join("");
2241
+ return /* @__PURE__ */ jsxs21("div", { className: `ui-app ui-app--topbar ui-react-app${collapsed ? " is-collapsed" : ""}`, children: [
2242
+ /* @__PURE__ */ jsxs21("div", { className: "ui-app__rail", children: [
2243
+ /* @__PURE__ */ jsx25("div", { className: "ui-app__head", children: /* @__PURE__ */ jsxs21("a", { className: "ui-app__brand", href: brandHref, "aria-label": word, children: [
2244
+ brand ?? /* @__PURE__ */ jsx25("i", { "aria-hidden": "true", className: "ui-react-app__mark", dangerouslySetInnerHTML: { __html: prism(uid, 24) } }),
2245
+ /* @__PURE__ */ jsx25("span", { children: word })
2246
+ ] }) }),
2247
+ /* @__PURE__ */ jsx25("nav", { className: "ui-nav ui-nav--side", "aria-label": "Sections", children: /* @__PURE__ */ jsx25("ul", { className: "ui-nav__list", children: links(sections) }) }),
2248
+ /* @__PURE__ */ jsx25("div", { className: "ui-app__foot", children: /* @__PURE__ */ jsx25("div", { className: "ui-app__fold-row", children: /* @__PURE__ */ jsxs21("button", { type: "button", className: "ui-nav__item ui-app__fold", "aria-expanded": !collapsed, "aria-label": foldLabel, onClick: fold, children: [
2249
+ /* @__PURE__ */ jsx25("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsxs21("svg", { className: "ui-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
2250
+ /* @__PURE__ */ jsx25("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
2251
+ /* @__PURE__ */ jsx25("path", { className: "ui-app__fold-seam", d: "M9 3v18" })
2252
+ ] }) }),
2253
+ /* @__PURE__ */ jsx25("span", { className: "ui-nav__label", children: foldLabel })
2254
+ ] }) }) })
2255
+ ] }),
2256
+ /* @__PURE__ */ jsxs21("div", { className: "ui-app__well", children: [
2257
+ /* @__PURE__ */ jsxs21("header", { className: "ui-app__bar", children: [
2258
+ /* @__PURE__ */ jsxs21("button", { type: "button", className: "ui-app__search", "aria-haspopup": "dialog", onClick: () => setSearch(true), children: [
2259
+ /* @__PURE__ */ jsx25("span", { className: "ui-app__search-ic", children: /* @__PURE__ */ jsx25(Icon, { name: "search" }) }),
2260
+ /* @__PURE__ */ jsx25("span", { className: "ui-app__search-txt", children: "Search or run a command\u2026" }),
2261
+ /* @__PURE__ */ jsx25("kbd", { className: "ui-cmdk__key", children: paletteHotkey() })
2262
+ ] }),
2263
+ /* @__PURE__ */ jsx25("span", { className: "ui-app__bar-gap" }),
2264
+ bandControl,
2265
+ /* @__PURE__ */ jsx25("button", { type: "button", className: "toggle", "aria-label": themeName(theme), title: themeName(theme), onClick: () => applyTheme(theme === "light" ? "dark" : "light"), children: /* @__PURE__ */ jsx25("span", { className: "ic", "aria-hidden": "true", dangerouslySetInnerHTML: { __html: themeIcon(theme) } }) }),
2266
+ /* @__PURE__ */ jsx25("div", { className: "ui-app__user ui-app__user--bar", children: /* @__PURE__ */ jsx25(
2267
+ Dropdown,
2268
+ {
2269
+ variant: "menu",
2270
+ align: "end",
2271
+ chevron: false,
2272
+ ariaLabel: `Signed in as ${account.name}, ${account.email}`,
2273
+ triggerClass: "ui-app__user-trigger",
2274
+ panelClass: "ui-app__user-panel",
2275
+ triggerContent: /* @__PURE__ */ jsx25("span", { className: "ui-app__av", "aria-hidden": "true", children: initials }),
2276
+ header: /* @__PURE__ */ jsxs21("div", { className: "ui-dropdown__head", children: [
2277
+ /* @__PURE__ */ jsx25("b", { children: account.name }),
2278
+ /* @__PURE__ */ jsx25("span", { children: account.email })
2279
+ ] }),
2280
+ items: [{ label: "Sign out", icon: "logout", danger: true }],
2281
+ onSelect: onSignOut
2282
+ }
2283
+ ) })
2284
+ ] }),
2285
+ /* @__PURE__ */ jsxs21("main", { className: `ui-app__main${width === "wide" ? " ui-app__main--wide" : ""}`, children: [
2286
+ back && /* @__PURE__ */ jsx25(BackLink, { ...back }),
2287
+ /* @__PURE__ */ jsx25("h1", { children: title }),
2288
+ lede && /* @__PURE__ */ jsx25("p", { className: "ui-app__sub", children: lede }),
2289
+ actions && /* @__PURE__ */ jsx25("div", { className: "ui-react-app__actions", children: actions }),
2290
+ /* @__PURE__ */ jsx25("div", { className: "ui-app__body", children })
2291
+ ] })
2292
+ ] }),
2293
+ /* @__PURE__ */ jsx25("nav", { className: "ui-nav ui-react-app__bottom", "aria-label": "Sections on mobile", children: /* @__PURE__ */ jsxs21("ul", { className: "ui-nav__list", children: [
2294
+ links(visible),
2295
+ remaining.length > 0 && /* @__PURE__ */ jsx25("li", { children: /* @__PURE__ */ jsxs21(
2296
+ "button",
2297
+ {
2298
+ type: "button",
2299
+ className: `ui-nav__item${active && remaining.includes(active) ? " is-active" : ""}`,
2300
+ "aria-haspopup": "dialog",
2301
+ "aria-expanded": more,
2302
+ onClick: () => setMore(true),
2303
+ children: [
2304
+ /* @__PURE__ */ jsx25("span", { className: "ui-nav__ic", children: /* @__PURE__ */ jsx25(Icon, { name: "moreHorizontal" }) }),
2305
+ /* @__PURE__ */ jsx25("span", { className: "ui-nav__label", children: "More" })
2306
+ ]
2307
+ }
2308
+ ) })
2309
+ ] }) }),
2310
+ /* @__PURE__ */ jsx25(Drawer, { open: more, onClose: () => setMore(false), title: "More sections", side: "bottom", children: /* @__PURE__ */ jsx25("nav", { className: "ui-nav ui-nav--side", "aria-label": "More sections", children: /* @__PURE__ */ jsx25("ul", { className: "ui-nav__list", children: links(remaining) }) }) }),
2311
+ /* @__PURE__ */ jsx25(CommandPalette, { ...palette ?? { groups: [{ items: sections.map((section) => ({ id: section.href, ...section })) }] }, open: search, onClose: () => setSearch(false) })
2312
+ ] });
2313
+ }
2314
+
2155
2315
  // src/index.ts
2156
2316
  var PAGE_SIZES = KIT_PAGE_SIZES;
2157
2317
  var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
2158
2318
  export {
2319
+ AppShell,
2159
2320
  BackLink,
2160
2321
  Badge,
2161
2322
  BusyRegion,