@toyonix/web-kit 0.1.0 → 0.1.2

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.
@@ -111,6 +111,35 @@
111
111
  outline: var(--tx-border-width) solid var(--tx-border);
112
112
  }
113
113
 
114
+ /* Narrow screens: app links get their own row under the logo, scrolling sideways if needed. */
115
+ @media (max-width: 640px) {
116
+ .tx-header__inner {
117
+ flex-wrap: wrap;
118
+ row-gap: 4px;
119
+ padding-top: 10px;
120
+ padding-bottom: 8px;
121
+ min-height: 0;
122
+ }
123
+
124
+ .tx-header__inner .tx-icon-btn {
125
+ margin-left: auto;
126
+ }
127
+
128
+ .tx-nav {
129
+ order: 3;
130
+ flex-wrap: nowrap;
131
+ width: 100%;
132
+ margin-left: 0;
133
+ padding: 4px 3px 6px;
134
+ overflow-x: auto;
135
+ scrollbar-width: none;
136
+ }
137
+
138
+ .tx-nav a {
139
+ flex: none;
140
+ }
141
+ }
142
+
114
143
  .tx-subnav {
115
144
  border-bottom: var(--tx-border-width) solid var(--tx-border);
116
145
  background: var(--tx-surface);
@@ -562,6 +591,27 @@
562
591
  font-style: italic;
563
592
  }
564
593
 
594
+ /* Wide images span the column; tall ones are capped at most of the viewport and centered. */
595
+ .tx-prose img {
596
+ display: block;
597
+ width: auto;
598
+ max-width: 100%;
599
+ height: auto;
600
+ max-height: min(80vh, 900px);
601
+ margin: 1.6em auto;
602
+ border: var(--tx-border-width) solid var(--tx-border);
603
+ box-shadow: var(--tx-shadow);
604
+ }
605
+
606
+ /* Section break: a short accent marker bar, centered. */
607
+ .tx-prose hr {
608
+ width: 96px;
609
+ height: 0;
610
+ margin: 2.6em auto;
611
+ border: 0;
612
+ border-top: 6px solid var(--tx-accent);
613
+ }
614
+
565
615
  .tx-dropcap::first-letter,
566
616
  .tx-prose--dropcap > p:first-of-type::first-letter {
567
617
  float: left;
@@ -870,9 +920,204 @@
870
920
  outline: var(--tx-border-width) solid var(--tx-border);
871
921
  }
872
922
 
923
+ /* ============================== table of contents ============================== */
924
+
925
+ /*
926
+ * Wide screens: article + sticky "On this page" column. Narrow screens: the column is hidden
927
+ * and a floating button opens the same list in a menu that slides in from the right.
928
+ */
929
+ .tx-with-toc {
930
+ display: grid;
931
+ grid-template-columns: minmax(0, 1fr);
932
+ gap: 64px;
933
+ align-items: start;
934
+ }
935
+
936
+ .tx-toc {
937
+ display: none;
938
+ }
939
+
940
+ @media (min-width: 1100px) {
941
+ .tx-with-toc {
942
+ grid-template-columns: minmax(0, 1fr) 240px;
943
+ }
944
+
945
+ .tx-toc {
946
+ display: block;
947
+ position: sticky;
948
+ top: 96px;
949
+ max-height: calc(100vh - 120px);
950
+ overflow-y: auto;
951
+ padding: 6px 4px 12px 0;
952
+ }
953
+
954
+ .tx-toc-trigger {
955
+ display: none !important;
956
+ }
957
+ }
958
+
959
+ .tx-toc__title {
960
+ margin-bottom: 18px;
961
+ font-size: 18px;
962
+ }
963
+
964
+ .tx-toc__list {
965
+ margin: 0;
966
+ padding: 0 0 0 14px;
967
+ border-left: var(--tx-border-width) solid var(--tx-border);
968
+ list-style: none;
969
+ }
970
+
971
+ .tx-toc__item {
972
+ margin: 0;
973
+ }
974
+
975
+ .tx-toc__item--sub {
976
+ padding-left: 14px;
977
+ }
978
+
979
+ .tx-toc__link {
980
+ display: block;
981
+ padding: 5px 0;
982
+ color: var(--tx-text-muted);
983
+ font-size: 14px;
984
+ line-height: 1.35;
985
+ text-decoration: none;
986
+ }
987
+
988
+ .tx-toc__item--sub .tx-toc__link {
989
+ font-size: 13px;
990
+ }
991
+
992
+ .tx-toc__link:hover {
993
+ color: var(--tx-text);
994
+ }
995
+
996
+ .tx-toc__link[aria-current] {
997
+ color: var(--tx-text);
998
+ font-weight: 600;
999
+ }
1000
+
1001
+ /* The current section gets the marker highlight, like <em> in headlines. */
1002
+ .tx-toc__link[aria-current] .tx-toc__text {
1003
+ background: linear-gradient(transparent 58%, var(--tx-accent) 58%, var(--tx-accent) 92%, transparent 92%);
1004
+ -webkit-box-decoration-break: clone;
1005
+ box-decoration-break: clone;
1006
+ }
1007
+
1008
+ .tx-toc-trigger {
1009
+ position: fixed;
1010
+ right: 16px;
1011
+ bottom: calc(16px + env(safe-area-inset-bottom));
1012
+ z-index: 40;
1013
+ font-size: 14px;
1014
+ }
1015
+
1016
+ .tx-toc-overlay {
1017
+ z-index: 60;
1018
+ }
1019
+
1020
+ .tx-toc-drawer {
1021
+ position: fixed;
1022
+ top: 0;
1023
+ right: 0;
1024
+ bottom: 0;
1025
+ z-index: 61;
1026
+ display: flex;
1027
+ flex-direction: column;
1028
+ width: min(340px, 86vw);
1029
+ padding: 20px 20px calc(24px + env(safe-area-inset-bottom));
1030
+ overflow-y: auto;
1031
+ border-left: var(--tx-border-width) solid var(--tx-border);
1032
+ background: var(--tx-surface);
1033
+ box-shadow: -6px 0 0 var(--tx-border);
1034
+ color: var(--tx-text);
1035
+ }
1036
+
1037
+ .tx-toc-drawer[data-state="open"] {
1038
+ animation: tx-slide-in-right 0.22s ease-out;
1039
+ }
1040
+
1041
+ .tx-toc-drawer[data-state="closed"] {
1042
+ animation: tx-slide-out-right 0.16s ease-in;
1043
+ }
1044
+
1045
+ .tx-toc-overlay[data-state="open"] {
1046
+ animation: tx-fade-in 0.2s ease-out;
1047
+ }
1048
+
1049
+ .tx-toc-overlay[data-state="closed"] {
1050
+ animation: tx-fade-out 0.16s ease-in;
1051
+ }
1052
+
1053
+ .tx-toc-drawer__head {
1054
+ display: flex;
1055
+ align-items: flex-start;
1056
+ justify-content: space-between;
1057
+ gap: 12px;
1058
+ margin-bottom: 4px;
1059
+ }
1060
+
1061
+ .tx-toc-drawer__close {
1062
+ padding: 6px 12px;
1063
+ }
1064
+
1065
+ .tx-toc-drawer .tx-toc__link {
1066
+ padding: 10px 0; /* bigger tap targets */
1067
+ font-size: 16px;
1068
+ }
1069
+
1070
+ .tx-toc-drawer .tx-toc__item--sub .tx-toc__link {
1071
+ font-size: 15px;
1072
+ }
1073
+
1074
+ @keyframes tx-slide-in-right {
1075
+ from {
1076
+ transform: translateX(100%);
1077
+ }
1078
+ }
1079
+
1080
+ @keyframes tx-slide-out-right {
1081
+ to {
1082
+ transform: translateX(100%);
1083
+ }
1084
+ }
1085
+
1086
+ @keyframes tx-fade-in {
1087
+ from {
1088
+ opacity: 0;
1089
+ }
1090
+ }
1091
+
1092
+ @keyframes tx-fade-out {
1093
+ to {
1094
+ opacity: 0;
1095
+ }
1096
+ }
1097
+
1098
+ /* Anchor jumps (table of contents, #links) land below the sticky header, not under it. */
1099
+ .tx-prose :is(h2, h3, h4) {
1100
+ scroll-margin-top: 96px;
1101
+ }
1102
+
1103
+ .tx-visually-hidden {
1104
+ position: absolute;
1105
+ width: 1px;
1106
+ height: 1px;
1107
+ overflow: hidden;
1108
+ clip: rect(0 0 0 0);
1109
+ clip-path: inset(50%);
1110
+ white-space: nowrap;
1111
+ }
1112
+
873
1113
  /* ============================== motion ============================== */
874
1114
 
875
1115
  @media (prefers-reduced-motion: reduce) {
1116
+ .tx-toc-drawer,
1117
+ .tx-toc-overlay {
1118
+ animation: none !important;
1119
+ }
1120
+
876
1121
  .tx-btn,
877
1122
  .tx-card--link,
878
1123
  .tx-switch,
@@ -0,0 +1,23 @@
1
+ //#region src/react/TableOfContents.d.ts
2
+ export interface TocItem {
3
+ /** The heading element's id (the anchor). */
4
+ id: string;
5
+ text: string;
6
+ /** 2 for `##`, 3 for `###`. Deeper levels are better left out. */
7
+ depth: 2 | 3;
8
+ }
9
+ export interface TableOfContentsProps {
10
+ items: TocItem[];
11
+ /** Below this many items nothing renders (a short note doesn't need an index). Default 3. */
12
+ minItems?: number;
13
+ title?: string;
14
+ className?: string;
15
+ }
16
+ /**
17
+ * "On this page" navigation for long articles. Place it as the second child of a `.tx-with-toc`
18
+ * grid, next to the article. Wide screens get a sticky column; narrow screens get a floating
19
+ * button that opens a menu sliding in from the right. Works without JS (plain anchor links);
20
+ * JS adds the current-section highlight and the mobile menu.
21
+ */
22
+ export declare function TableOfContents({ items, minItems, title, className }: TableOfContentsProps): import("react").JSX.Element | null;
23
+ //#endregion
@@ -0,0 +1,135 @@
1
+ "use client";
2
+ import { cx } from "./cx.js";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Dialog } from "radix-ui";
6
+ //#region src/react/TableOfContents.tsx
7
+ /** How far below the viewport top a heading counts as "current" (clears the sticky header). */
8
+ const ACTIVE_OFFSET = 120;
9
+ /** The id of the last heading scrolled past, or the first one before any. */
10
+ function useActiveHeading(ids) {
11
+ const [active, setActive] = useState(ids[0]);
12
+ useEffect(() => {
13
+ if (ids.length === 0) return;
14
+ let frame = 0;
15
+ const update = () => {
16
+ frame = 0;
17
+ let current = ids[0];
18
+ for (const id of ids) {
19
+ const element = document.getElementById(id);
20
+ if (element && element.getBoundingClientRect().top <= ACTIVE_OFFSET) current = id;
21
+ }
22
+ setActive(current);
23
+ };
24
+ const onScroll = () => {
25
+ if (!frame) frame = requestAnimationFrame(update);
26
+ };
27
+ update();
28
+ window.addEventListener("scroll", onScroll, { passive: true });
29
+ window.addEventListener("resize", onScroll);
30
+ return () => {
31
+ window.removeEventListener("scroll", onScroll);
32
+ window.removeEventListener("resize", onScroll);
33
+ if (frame) cancelAnimationFrame(frame);
34
+ };
35
+ }, [ids]);
36
+ return active;
37
+ }
38
+ function TocList({ items, active, onNavigate }) {
39
+ return /* @__PURE__ */ jsx("ol", {
40
+ className: "tx-toc__list",
41
+ children: items.map((item) => /* @__PURE__ */ jsx("li", {
42
+ className: cx("tx-toc__item", item.depth === 3 && "tx-toc__item--sub"),
43
+ children: /* @__PURE__ */ jsx("a", {
44
+ className: "tx-toc__link",
45
+ href: `#${item.id}`,
46
+ "aria-current": item.id === active ? "location" : void 0,
47
+ onClick: onNavigate ? (event) => onNavigate(event, item.id) : void 0,
48
+ children: /* @__PURE__ */ jsx("span", {
49
+ className: "tx-toc__text",
50
+ children: item.text
51
+ })
52
+ })
53
+ }, item.id))
54
+ });
55
+ }
56
+ /**
57
+ * "On this page" navigation for long articles. Place it as the second child of a `.tx-with-toc`
58
+ * grid, next to the article. Wide screens get a sticky column; narrow screens get a floating
59
+ * button that opens a menu sliding in from the right. Works without JS (plain anchor links);
60
+ * JS adds the current-section highlight and the mobile menu.
61
+ */
62
+ function TableOfContents({ items, minItems = 3, title = "On this page", className }) {
63
+ const [open, setOpen] = useState(false);
64
+ const [pending, setPending] = useState(null);
65
+ const idKey = items.map((item) => item.id).join("\n");
66
+ const active = useActiveHeading(useMemo(() => idKey ? idKey.split("\n") : [], [idKey]));
67
+ const navigateFromMenu = useCallback((event, id) => {
68
+ event.preventDefault();
69
+ setPending(id);
70
+ setOpen(false);
71
+ }, []);
72
+ const finishNavigation = useCallback((event) => {
73
+ if (!pending) return;
74
+ event.preventDefault();
75
+ const target = document.getElementById(pending);
76
+ setPending(null);
77
+ if (!target) return;
78
+ history.pushState(null, "", `#${pending}`);
79
+ target.scrollIntoView({ block: "start" });
80
+ }, [pending]);
81
+ if (items.length < minItems) return null;
82
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("nav", {
83
+ className: cx("tx-toc", className),
84
+ "aria-label": title,
85
+ children: [/* @__PURE__ */ jsx("p", {
86
+ className: "tx-section-label tx-toc__title",
87
+ children: title
88
+ }), /* @__PURE__ */ jsx(TocList, {
89
+ items,
90
+ active
91
+ })]
92
+ }), /* @__PURE__ */ jsxs(Dialog.Root, {
93
+ open,
94
+ onOpenChange: setOpen,
95
+ children: [/* @__PURE__ */ jsxs(Dialog.Trigger, {
96
+ className: "tx-btn tx-btn--primary tx-toc-trigger",
97
+ "aria-label": `Open: ${title}`,
98
+ children: [
99
+ /* @__PURE__ */ jsx("span", {
100
+ "aria-hidden": "true",
101
+ children: "☰"
102
+ }),
103
+ " ",
104
+ title
105
+ ]
106
+ }), /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "tx-overlay tx-toc-overlay" }), /* @__PURE__ */ jsxs(Dialog.Content, {
107
+ className: "tx-toc-drawer",
108
+ onCloseAutoFocus: finishNavigation,
109
+ children: [
110
+ /* @__PURE__ */ jsxs("div", {
111
+ className: "tx-toc-drawer__head",
112
+ children: [/* @__PURE__ */ jsx(Dialog.Title, {
113
+ className: "tx-section-label tx-toc__title",
114
+ children: title
115
+ }), /* @__PURE__ */ jsx(Dialog.Close, {
116
+ className: "tx-btn tx-btn--ghost tx-toc-drawer__close",
117
+ "aria-label": "Close",
118
+ children: "✕"
119
+ })]
120
+ }),
121
+ /* @__PURE__ */ jsx(Dialog.Description, {
122
+ className: "tx-visually-hidden",
123
+ children: "Jump to a section of this page."
124
+ }),
125
+ /* @__PURE__ */ jsx(TocList, {
126
+ items,
127
+ active,
128
+ onNavigate: navigateFromMenu
129
+ })
130
+ ]
131
+ })] })]
132
+ })] });
133
+ }
134
+ //#endregion
135
+ export { TableOfContents };
@@ -18,4 +18,5 @@ import { Dialog, DialogClose, DialogContent, DialogContentProps, DialogTrigger }
18
18
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./DropdownMenu.js";
19
19
  import { Tooltip, TooltipProps } from "./Tooltip.js";
20
20
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
21
- export { Alert, type AlertTone, AppLink, type AppLinkProps, AppShell, type AppShellProps, Badge, type BadgeTone, Badges, Button, type ButtonProps, ButtonRow, type ButtonVariant, Card, CardBody, CardCta, CardGrid, CardMeta, type CardProps, CardTitle, CardTop, Checkbox, CodeBlock, Container, Dialog, DialogClose, DialogContent, type DialogContentProps, DialogTrigger, Display, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Eyebrow, Field, Form, Input, Lead, LinkButton, type LinkButtonProps, LinkCard, Mark, Prose, Radio, Row, Section, SectionLabel, Select, Stack, Switch, Table, Tabs, TabsContent, TabsList, TabsTrigger, Tag, Tags, Textarea, ThemeScript, type ThemeState, ThemeToggle, Tooltip, type TooltipProps, cx, useRegistry, useTheme };
21
+ import { TableOfContents, TableOfContentsProps, TocItem } from "./TableOfContents.js";
22
+ export { Alert, type AlertTone, AppLink, type AppLinkProps, AppShell, type AppShellProps, Badge, type BadgeTone, Badges, Button, type ButtonProps, ButtonRow, type ButtonVariant, Card, CardBody, CardCta, CardGrid, CardMeta, type CardProps, CardTitle, CardTop, Checkbox, CodeBlock, Container, Dialog, DialogClose, DialogContent, type DialogContentProps, DialogTrigger, Display, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Eyebrow, Field, Form, Input, Lead, LinkButton, type LinkButtonProps, LinkCard, Mark, Prose, Radio, Row, Section, SectionLabel, Select, Stack, Switch, Table, TableOfContents, type TableOfContentsProps, Tabs, TabsContent, TabsList, TabsTrigger, Tag, Tags, Textarea, ThemeScript, type ThemeState, ThemeToggle, type TocItem, Tooltip, type TooltipProps, cx, useRegistry, useTheme };
@@ -18,4 +18,5 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger } from "./Dialog.js";
18
18
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./DropdownMenu.js";
19
19
  import { Tooltip } from "./Tooltip.js";
20
20
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
21
- export { Alert, AppLink, AppShell, Badge, Badges, Button, ButtonRow, Card, CardBody, CardCta, CardGrid, CardMeta, CardTitle, CardTop, Checkbox, CodeBlock, Container, Dialog, DialogClose, DialogContent, DialogTrigger, Display, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Eyebrow, Field, Form, Input, Lead, LinkButton, LinkCard, Mark, Prose, Radio, Row, Section, SectionLabel, Select, Stack, Switch, Table, Tabs, TabsContent, TabsList, TabsTrigger, Tag, Tags, Textarea, ThemeScript, ThemeToggle, Tooltip, cx, useRegistry, useTheme };
21
+ import { TableOfContents } from "./TableOfContents.js";
22
+ export { Alert, AppLink, AppShell, Badge, Badges, Button, ButtonRow, Card, CardBody, CardCta, CardGrid, CardMeta, CardTitle, CardTop, Checkbox, CodeBlock, Container, Dialog, DialogClose, DialogContent, DialogTrigger, Display, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Eyebrow, Field, Form, Input, Lead, LinkButton, LinkCard, Mark, Prose, Radio, Row, Section, SectionLabel, Select, Stack, Switch, Table, TableOfContents, Tabs, TabsContent, TabsList, TabsTrigger, Tag, Tags, Textarea, ThemeScript, ThemeToggle, Tooltip, cx, useRegistry, useTheme };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toyonix/web-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Shared design system, components, and tooling for every toyonix static site.",
5
5
  "license": "MIT",
6
6
  "author": "ToYoNiX",