tamga-ui 0.5.9 → 0.6.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/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ Versions follow [semver](https://semver.org/). Through `0.x`, breaking changes m
5
5
 
6
6
  > 🇹🇷 Türkçe için [tamga.org.tr/tr](https://tamga.org.tr/tr).
7
7
 
8
+ ## 0.6.0
9
+
10
+ > Sections fold; the one you are in never does.
11
+
12
+ ### Added
13
+
14
+ - **`AppShell` section headers fold.** In the wide rail every section header is now a button:
15
+ click it and the section's entries slide shut; click again and they return. A customer with
16
+ eight sections asked for exactly this — the rail had become a wall. Three rules keep it honest:
17
+ the open page's section never folds (you can always read where you are), a route that lands
18
+ inside a folded section finds it open, and the narrow rail is untouched (nothing to fold when
19
+ there are no headers). New props: `collapsibleSections` (default `true`), `collapsedSections`
20
+ for a controlled rail, `onSectionToggle(section, open)`. Where the preference lives — browser,
21
+ account, device — is the product's call; the shell never writes storage.
22
+
23
+ ### Breaking
24
+
25
+ - Section headers in the wide rail are `<button aria-expanded>` elements instead of `<p>`, and
26
+ each section's links now sit inside a `.tamga-rail-group-body` wrapper rather than directly
27
+ under `<nav>`. A selector written as `nav > .tamga-rail-link` or a test that read the header
28
+ as a paragraph needs updating; `data-nav` on every link is unchanged and is the supported hook.
29
+ Pass `collapsibleSections={false}` to keep plain headers (the wrapper stays either way).
30
+
8
31
  ## 0.5.9
9
32
 
10
33
  > The hook lands on the root, in every branch.
package/dist/fonts.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! tamga-ui 0.5.9 · MIT · https://tamga.org.tr */
1
+ /*! tamga-ui 0.6.0 · MIT · https://tamga.org.tr */
2
2
  @font-face {
3
3
  font-family: "Red Hat Display";
4
4
  font-style: normal;
package/dist/kit.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! tamga-ui 0.5.9 · MIT · https://tamga.org.tr */
1
+ /*! tamga-ui 0.6.0 · MIT · https://tamga.org.tr */
2
2
  :root {
3
3
 
4
4
  --overlay-gap: 8px;
@@ -380,6 +380,69 @@
380
380
  display: none;
381
381
  }
382
382
 
383
+ .tamga-rail-section {
384
+ align-items: center;
385
+ background: transparent;
386
+ border: 0;
387
+ border-radius: var(--radius-ctl);
388
+ color: var(--color-ink-faint);
389
+ cursor: pointer;
390
+ display: flex;
391
+ font: inherit;
392
+ font-size: var(--text-caption);
393
+ font-weight: 700;
394
+ gap: 6px;
395
+ letter-spacing: var(--tracking-label);
396
+ padding: 4px 10px;
397
+ text-align: left;
398
+ text-transform: uppercase;
399
+ transition: background var(--duration-press) var(--ease-standard), color var(--duration-press) var(--ease-standard);
400
+ width: 100%;
401
+ }
402
+ .tamga-rail-section:hover {
403
+ background: var(--color-hover);
404
+ color: var(--color-ink);
405
+ }
406
+ .tamga-rail-section[data-locked="true"] {
407
+ cursor: default;
408
+ }
409
+ .tamga-rail-section[data-locked="true"]:hover {
410
+ background: transparent;
411
+ color: var(--color-ink-faint);
412
+ }
413
+ .tamga-rail-section-caret {
414
+ margin-left: auto;
415
+ transition: transform var(--duration-press) var(--ease-standard), opacity var(--duration-press) var(--ease-standard);
416
+ }
417
+ .tamga-rail-section[aria-expanded="false"] .tamga-rail-section-caret {
418
+ transform: rotate(-90deg);
419
+ }
420
+ .tamga-rail-section[data-locked="true"] .tamga-rail-section-caret {
421
+ opacity: 0.35;
422
+ }
423
+
424
+ .tamga-rail-group-body {
425
+ display: grid;
426
+ grid-template-rows: 1fr;
427
+ transition: grid-template-rows var(--duration-press) var(--ease-standard);
428
+ }
429
+ .tamga-rail-group-body > div {
430
+ display: flex;
431
+ flex-direction: column;
432
+ gap: 8px;
433
+ min-height: 0;
434
+ overflow: hidden;
435
+ }
436
+ .tamga-rail-group-body[data-open="false"] {
437
+ grid-template-rows: 0fr;
438
+ }
439
+ @media (prefers-reduced-motion: reduce) {
440
+ .tamga-rail-group-body,
441
+ .tamga-rail-section-caret {
442
+ transition: none;
443
+ }
444
+ }
445
+
383
446
  .tamga-rail-link {
384
447
  position: relative;
385
448
  display: flex;
@@ -79,11 +79,30 @@ export type AppShellProps = {
79
79
  */
80
80
  mainClassName?: string;
81
81
  linkComponent?: LinkComponent;
82
+ /**
83
+ * Section headers fold. On, each header in the wide rail is a button that hides its entries; the
84
+ * open page's section never folds, and a route landing inside a folded section finds it open.
85
+ * Off, headers are plain labels, the pre-0.6 rail. TR: Bölüm başlıkları katlanır. Açıkken geniş
86
+ * raydaki her başlık girişlerini gizleyen bir düğme; açık sayfanın bölümü hiç katlanmaz, kapalı
87
+ * bir bölüme düşen rota onu açık bulur. Kapalıyken başlıklar düz etiket, 0.6 öncesi ray.
88
+ */
89
+ collapsibleSections?: boolean;
90
+ /**
91
+ * The folded sections, by name. Given, the shell is controlled: a click only reports through
92
+ * `onSectionToggle`, and the product decides. Absent, the shell keeps the state itself and forgets
93
+ * it on reload · where a preference lives (browser, account, device) is the product's call.
94
+ * TR: Kapalı bölümler, adıyla. Verilirse kabuk kontrollü: tık yalnız `onSectionToggle` ile haber
95
+ * verir, kararı ürün verir. Verilmezse kabuk durumu kendi tutar ve yenilemede unutur · tercihin
96
+ * nerede yaşadığı (tarayıcı, hesap, cihaz) ürünün kararı.
97
+ */
98
+ collapsedSections?: readonly string[];
99
+ /** Fires with the section name and whether it is now open. TR: Bölüm adı ve artık açık olup olmadığıyla ateşlenir. */
100
+ onSectionToggle?: (section: string, open: boolean) => void;
82
101
  labels: {
83
102
  home: string;
84
103
  primaryNav: string;
85
104
  };
86
105
  children: ReactNode;
87
106
  };
88
- export declare function AppShell({ brand, nav, activePath, rail, topbar, railFooter, mainClassName, linkComponent: Link, labels, children, }: AppShellProps): import("react").JSX.Element;
107
+ export declare function AppShell({ brand, nav, activePath, rail, topbar, railFooter, mainClassName, linkComponent: Link, collapsibleSections, collapsedSections, onSectionToggle, labels, children, }: AppShellProps): import("react").JSX.Element;
89
108
  //# sourceMappingURL=app-shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../../src/patterns/app-shell.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAG7C,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAAG;IACrB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,wKAAwK;IACxK,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1C;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,wJAAwJ;IACxJ,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,SAAS,QAAQ,EAAE,CAAC;IACzB,mGAAmG;IACnG,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,uIAAuI;IACvI,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,GAAG,EACH,UAAU,EACV,IAAe,EACf,MAAM,EACN,UAAU,EACV,aAAa,EACb,aAAa,EAAE,IAAgB,EAC/B,MAAM,EACN,QAAQ,GACT,EAAE,aAAa,+BA+Hf"}
1
+ {"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../../src/patterns/app-shell.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAI7C,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAAG;IACrB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,wKAAwK;IACxK,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1C;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,wJAAwJ;IACxJ,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,SAAS,QAAQ,EAAE,CAAC;IACzB,mGAAmG;IACnG,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,uIAAuI;IACvI,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,sHAAsH;IACtH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3D,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,GAAG,EACH,UAAU,EACV,IAAe,EACf,MAAM,EACN,UAAU,EACV,aAAa,EACb,aAAa,EAAE,IAAgB,EAC/B,mBAA0B,EAC1B,iBAAiB,EACjB,eAAe,EACf,MAAM,EACN,QAAQ,GACT,EAAE,aAAa,+BA+Kf"}
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Fragment } from "react";
3
+ import { Fragment, useId, useState } from "react";
4
4
  import { Icon } from "../components/icon.js";
5
+ import { CaretDown } from "../components/icons.js";
5
6
  import { RailLink } from "../components/rail-link.js";
6
7
  import { cn } from "../lib/cn.js";
7
8
  import { PlainLink } from "./shared.js";
8
- export function AppShell({ brand, nav, activePath, rail = "narrow", topbar, railFooter, mainClassName, linkComponent: Link = PlainLink, labels, children, }) {
9
+ export function AppShell({ brand, nav, activePath, rail = "narrow", topbar, railFooter, mainClassName, linkComponent: Link = PlainLink, collapsibleSections = true, collapsedSections, onSectionToggle, labels, children, }) {
9
10
  /* Bir giriş kendi alt ağacına sahip, ve EN UZUN EŞLEŞEN KAZANIR: kökün `/`
10
11
  olmadığı bir panelde kök giriş altındaki her rotayı yutuyordu.
11
12
  Gerekçe: docs/gerekce/08-blok-ve-sablon.md */
@@ -16,5 +17,38 @@ export function AppShell({ brand, nav, activePath, rail = "narrow", topbar, rail
16
17
  /* ADRESİ OLMAYAN GİRİŞ ANAHTARIYLA EŞLEŞİYOR: masaüstü kabuğunda ekran bir durum, yol değil. */
17
18
  const isCurrent = (entry) => entry.href ? entry.href === enOzel : entry.key === activePath;
18
19
  const wide = rail === "wide";
19
- return (_jsxs("div", { className: "flex h-dvh w-full overflow-hidden bg-rail text-ink-soft", children: [_jsxs("aside", { className: `flex shrink-0 flex-col gap-2 bg-rail py-4 ${wide ? "w-60 px-3" : "w-20 items-center px-1"}`, "data-rail": rail, children: [brand ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-label": labels.home, className: `mb-2 flex h-10 shrink-0 items-center text-ink ${wide ? "px-2" : "w-10 justify-center"}`, children: brand }), _jsx("span", { className: `h-0 w-6 border-t border-line ${wide ? "ml-2" : ""}` })] })) : null, _jsx("nav", { "aria-label": labels.primaryNav, className: `tamga-rail-scroll flex w-full flex-1 flex-col gap-2 py-1 ${wide ? "" : "items-center"}`, children: nav.map((entry, i) => (_jsxs(Fragment, { children: [entry.section && entry.section !== nav[i - 1]?.section ? (wide ? (_jsx("p", { className: `px-2.5 text-caption font-bold uppercase tracking-label text-ink-faint ${i > 0 ? "mt-5 pb-1.5" : "pb-1.5"}`, children: entry.section })) : i > 0 ? (_jsx("span", { className: "my-1.5 h-0 w-6 shrink-0 border-t border-line", "aria-hidden": true })) : null) : null, _jsx(RailLink, { href: entry.href, onClick: entry.onSelect, label: entry.label, active: isCurrent(entry), showLabel: wide, linkComponent: Link, "data-nav": entry.key, children: _jsx(Icon, { icon: entry.icon, size: "md", weight: isCurrent(entry) ? "fill" : "regular" }) })] }, entry.key))) }), railFooter ? _jsx("div", { className: "shrink-0", children: railFooter }) : null] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("header", { className: "tamga-gutter flex items-center gap-4 bg-band py-4", children: topbar }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden pr-4 pb-4", children: _jsx("main", { className: cn("tamga-surface tamga-app-main relative h-full min-h-0 overflow-y-auto", mainClassName), children: children }) })] })] }));
20
+ /* KATLAMA. Kapalı liste ya üründen gelir (kontrollü) ya da burada tutulur.
21
+ AÇIK SAYFANIN BÖLÜMÜ HEP AÇIK: "neredeyim" raydan okunmalı · kapalı bir
22
+ bölüme yönlendirmeyle düşen kullanıcı da onu açık bulur, listeyi
23
+ değiştirmeden. Dar rayda başlık yok, dolayısıyla katlanacak şey de yok. */
24
+ const [icKapali, setIcKapali] = useState([]);
25
+ const kapali = collapsedSections ?? icKapali;
26
+ const aktifBolum = nav.find(isCurrent)?.section;
27
+ const acikMi = (bolum) => !wide || !collapsibleSections || bolum === aktifBolum || !kapali.includes(bolum);
28
+ const degistir = (bolum) => {
29
+ if (bolum === aktifBolum)
30
+ return;
31
+ const sonraki = !acikMi(bolum);
32
+ onSectionToggle?.(bolum, sonraki);
33
+ if (collapsedSections === undefined) {
34
+ setIcKapali((k) => (sonraki ? k.filter((x) => x !== bolum) : [...k, bolum]));
35
+ }
36
+ };
37
+ /* Ardışık girişler bölüm adına göre kümeleniyor; adı olmayanlar kendi
38
+ kümesinde, başlıksız. Veri düz kalıyor (bkz. `section`), küme çizim için. */
39
+ const gruplar = [];
40
+ for (const entry of nav) {
41
+ const son = gruplar[gruplar.length - 1];
42
+ if (son && son.bolum === entry.section)
43
+ son.girisler.push(entry);
44
+ else
45
+ gruplar.push({ bolum: entry.section, girisler: [entry] });
46
+ }
47
+ const kimlik = useId();
48
+ return (_jsxs("div", { className: "flex h-dvh w-full overflow-hidden bg-rail text-ink-soft", children: [_jsxs("aside", { className: `flex shrink-0 flex-col gap-2 bg-rail py-4 ${wide ? "w-60 px-3" : "w-20 items-center px-1"}`, "data-rail": rail, children: [brand ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-label": labels.home, className: `mb-2 flex h-10 shrink-0 items-center text-ink ${wide ? "px-2" : "w-10 justify-center"}`, children: brand }), _jsx("span", { className: `h-0 w-6 border-t border-line ${wide ? "ml-2" : ""}` })] })) : null, _jsx("nav", { "aria-label": labels.primaryNav, className: `tamga-rail-scroll flex w-full flex-1 flex-col gap-2 py-1 ${wide ? "" : "items-center"}`, children: gruplar.map((grup, gi) => {
49
+ const id = `${kimlik}-${gi}`;
50
+ const acik = grup.bolum ? acikMi(grup.bolum) : true;
51
+ const kilitli = grup.bolum !== undefined && grup.bolum === aktifBolum;
52
+ return (_jsxs(Fragment, { children: [grup.bolum ? (wide ? (collapsibleSections ? (_jsxs("button", { type: "button", className: cn("tamga-rail-section", gi > 0 && "mt-3"), "aria-expanded": acik, "aria-controls": id, "aria-disabled": kilitli || undefined, "data-locked": kilitli || undefined, onClick: () => degistir(grup.bolum), children: [grup.bolum, _jsx(Icon, { icon: CaretDown, size: "xs", weight: "bold", className: "tamga-rail-section-caret" })] })) : (_jsx("p", { className: `px-2.5 text-caption font-bold uppercase tracking-label text-ink-faint ${gi > 0 ? "mt-5 pb-1.5" : "pb-1.5"}`, children: grup.bolum }))) : gi > 0 ? (_jsx("span", { className: "my-1.5 h-0 w-6 shrink-0 border-t border-line", "aria-hidden": true })) : null) : null, _jsx("div", { id: id, className: "tamga-rail-group-body", "data-open": acik, inert: !acik, children: _jsx("div", { className: wide ? undefined : "items-center", children: grup.girisler.map((entry) => (_jsx(RailLink, { href: entry.href, onClick: entry.onSelect, label: entry.label, active: isCurrent(entry), showLabel: wide, linkComponent: Link, "data-nav": entry.key, children: _jsx(Icon, { icon: entry.icon, size: "md", weight: isCurrent(entry) ? "fill" : "regular" }) }, entry.key))) }) })] }, grup.bolum ?? `_${gi}`));
53
+ }) }), railFooter ? _jsx("div", { className: "shrink-0", children: railFooter }) : null] }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("header", { className: "tamga-gutter flex items-center gap-4 bg-band py-4", children: topbar }), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden pr-4 pb-4", children: _jsx("main", { className: cn("tamga-surface tamga-app-main relative h-full min-h-0 overflow-y-auto", mainClassName), children: children }) })] })] }));
20
54
  }
package/dist/styles.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! tamga-ui 0.5.9 · MIT · https://tamga.org.tr */
1
+ /*! tamga-ui 0.6.0 · MIT · https://tamga.org.tr */
2
2
  @import "./fonts.css";
3
3
  @import "./theme.css";
4
4
  @import "./kit.css";
package/dist/theme.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! tamga-ui 0.5.9 · MIT · https://tamga.org.tr */
1
+ /*! tamga-ui 0.6.0 · MIT · https://tamga.org.tr */
2
2
  @custom-variant dark (&:is(.dark *));
3
3
 
4
4
  @theme static {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamga-ui",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "Mant\u0131ksal's shared interface library: tokens, physics and components.",
5
5
  "license": "MIT",
6
6
  "type": "module",