@krizaka/ui 3.48.0 → 3.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.
package/dist/sites.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sites/sites.ts"],"sourcesContent":["// Server-safe: plain data. The Krizaka family — every site Krizaka runs, once. The family switcher of the bar\n// (`NavBar.Family`) and the common footer (`SiteFooter`) both read it, so adding a site is one release of this package:\n// every bar and every footer of the family follow.\n//\n// The words of the family (a site's one line, the groups, the statuses, the footer's signature) are brand data, written\n// here in English and French (`familyLabels`) — a site passes the set of its locale, or its own translation.\n\n/** A site of the family. */\nexport type SiteId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"ui\" | \"java\" | \"motion\" | \"github\";\n\n/** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */\nexport type SiteKind = \"company\" | \"product\" | \"open-source\";\n\n/**\n * `released`: open to visit. `upcoming`: announced, not released yet (said \"Coming soon\"). `construction`: under\n * construction — nothing offered (a QA-only app may run; the public page says so). `development`: being built in the\n * open, with a dev site (said \"In development\"). A site with none yet would use its repository as its address.\n */\nexport type SiteStatus = \"released\" | \"upcoming\" | \"construction\" | \"development\";\n\n/** The groups of the family, in reading order: Krizaka · Products · Open source · GitHub. */\nexport type SiteGroup = \"krizaka\" | \"products\" | \"open-source\" | \"github\";\n\n/** The mark a site wears: a brand of `ProductLogo` (Krizaka UI and Krizaka Java wear Krizaka's), or GitHub's. */\nexport type SiteMark = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"github\";\n\nexport interface Site {\n readonly id: SiteId;\n /** The name, never translated. */\n readonly name: string;\n /** The address of the site (its home). */\n readonly url: string;\n /** The address without the scheme, as people say it (`ui.krizaka.com`). */\n readonly host: string;\n readonly kind: SiteKind;\n readonly group: SiteGroup;\n readonly status: SiteStatus;\n readonly mark: SiteMark;\n /** The repository the site is built from. */\n readonly repo: string;\n}\n\nconst site = (s: Omit<Site, \"host\">): Site => ({ ...s, host: s.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\") });\n\n/** Every site of the family, in reading order (the group order, then the order inside each group). */\nexport const sites: readonly Site[] = [\n site({ id: \"krizaka\", name: \"Krizaka\", url: \"https://www.krizaka.com\", kind: \"company\", group: \"krizaka\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-com\" }),\n site({ id: \"orazaka\", name: \"Orazaka\", url: \"https://www.orazaka.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orazaka\", repo: \"https://github.com/krizaka/orazaka\" }),\n site({ id: \"orochia\", name: \"Orochia\", url: \"https://www.orochia.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orochia\", repo: \"https://github.com/krizaka/orochia\" }),\n site({ id: \"yasakia\", name: \"Yasakia\", url: \"https://www.yasakia.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yasakia\", repo: \"https://github.com/krizaka/yasakia\" }),\n site({ id: \"yororis\", name: \"Yororis\", url: \"https://www.yororis.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yororis\", repo: \"https://github.com/krizaka/yororis\" }),\n site({ id: \"yomizaka\", name: \"Yomizaka\", url: \"https://www.yomizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yomizaka\", repo: \"https://github.com/krizaka/yomizaka\" }),\n site({ id: \"kachizaka\", name: \"Kachizaka\", url: \"https://www.kachizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"kachizaka\", repo: \"https://github.com/krizaka/kachizaka\" }),\n site({ id: \"akinaza\", name: \"Akinaza\", url: \"https://www.akinaza.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"akinaza\", repo: \"https://github.com/krizaka/akinaza\" }),\n site({ id: \"ui\", name: \"Krizaka UI\", url: \"https://ui.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ui\" }),\n site({ id: \"java\", name: \"Krizaka Java\", url: \"https://java.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-build\" }),\n // The motion lab: built in the open on GitHub Pages; what proves itself there moves into Krizaka UI.\n site({ id: \"motion\", name: \"Krizaka Motion\", url: \"https://krizaka.github.io/krizaka-motion\", kind: \"open-source\", group: \"open-source\", status: \"development\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-motion\" }),\n site({ id: \"github\", name: \"GitHub\", url: \"https://github.com/krizaka\", kind: \"open-source\", group: \"github\", status: \"released\", mark: \"github\", repo: \"https://github.com/krizaka\" }),\n];\n\n/** The groups, in reading order. */\nexport const siteGroups: readonly SiteGroup[] = [\"krizaka\", \"products\", \"open-source\", \"github\"];\n\n/** A site by id. */\nexport function siteById(id: SiteId): Site {\n const found = sites.find((s) => s.id === id);\n if (!found) throw new Error(`Unknown Krizaka site \"${id}\".`);\n return found;\n}\n\n/** The family grouped, in reading order: `[{ group, sites }]`, empty groups left out. */\nexport function sitesByGroup(): { group: SiteGroup; sites: Site[] }[] {\n return siteGroups.map((group) => ({ group, sites: sites.filter((s) => s.group === group) })).filter((g) => g.sites.length > 0);\n}\n\n/** The languages the family's words are written in. */\nexport type SiteLocale = \"en\" | \"fr\";\n\n/** The company's own pages, the footer's « Krizaka » column (the brand's home is the signature line's link). */\nexport type CompanyLinkId = \"story\" | \"blog\" | \"contact\";\nexport const companyLinks: readonly { id: CompanyLinkId; url: Record<SiteLocale, string> }[] = [\n { id: \"story\", url: { en: \"https://www.krizaka.com/story\", fr: \"https://www.krizaka.com/fr/story\" } },\n { id: \"blog\", url: { en: \"https://blog.krizaka.com\", fr: \"https://blog.krizaka.com\" } },\n { id: \"contact\", url: { en: \"https://www.krizaka.com/contact\", fr: \"https://www.krizaka.com/fr/contact\" } },\n];\n\n/** The words of the family: the switcher, the footer, each site's one line. */\nexport interface FamilyLabels {\n /** The family's name: the switcher's navigation and its button, the footer's navigation (“Krizaka family”). */\n family: string;\n /** The heading of each group. */\n groups: Record<SiteGroup, string>;\n /** The badge of a site that is not released. */\n status: Record<Exclude<SiteStatus, \"released\">, string>;\n /** Says which site is the current one (“You are here”). */\n current: string;\n /** One line per site: what it is. */\n descriptions: Record<SiteId, string>;\n /** The footer's signature, by the kind of the current site (“A Krizaka product”). */\n signature: Record<SiteKind, string>;\n /** The footer's place line (“Made in Montréal”). */\n place: string;\n /** The accessible name of the version (“Version”). */\n version: string;\n /** The language of these words: the company links open in it. @default \"en\" */\n lang?: SiteLocale;\n /** The company's pages in the footer's « Krizaka » column. @default the English words */\n company?: Record<CompanyLinkId, string>;\n}\n\n/** The family's words in English and French. Pass `familyLabels[locale]`, or your own translation of the same shape. */\nexport const familyLabels: Record<SiteLocale, FamilyLabels> = {\n en: {\n family: \"Krizaka family\",\n groups: { krizaka: \"Krizaka\", products: \"Products\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Coming soon\", construction: \"Under construction\", development: \"In development\" },\n current: \"You are here\",\n descriptions: {\n krizaka: \"The company: our products, our open source, our story.\",\n orazaka: \"Sovereign AI on your own servers: chat, documents, images and video — and the engine under the other products.\",\n orochia: \"A video platform for creators.\",\n yasakia: \"Meet real people: introductions, and photos checked before you see them.\",\n yororis: \"A listening space with a face and a voice.\",\n yomizaka: \"Yomizaka reads your leases, contracts and invoices.\",\n kachizaka: \"Predictions among friends — live data, in real time.\",\n akinaza: \"Running a shop in Québec, for less — Loi 25 by design.\",\n ui: \"Components, tokens, icons and charts — web and React Native.\",\n java: \"Spring Boot building blocks on Maven Central.\",\n motion: \"The motion lab: the kit's motion, played live with its knobs.\",\n github: \"Every repository, in the open.\",\n },\n signature: { company: \"A Krizaka company\", product: \"A Krizaka product\", \"open-source\": \"A Krizaka open-source project\" },\n place: \"Made in Montréal\",\n version: \"Version\",\n lang: \"en\",\n company: { story: \"Our story\", blog: \"Blog\", contact: \"Contact us\" },\n },\n fr: {\n family: \"Famille Krizaka\",\n groups: { krizaka: \"Krizaka\", products: \"Produits\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Bientôt\", construction: \"En construction\", development: \"En développement\" },\n current: \"Vous êtes ici\",\n descriptions: {\n krizaka: \"La compagnie : nos produits, notre open source, notre histoire.\",\n orazaka: \"L’IA souveraine sur vos propres serveurs : conversation, documents, images et vidéo — et le moteur des autres produits.\",\n orochia: \"Une plateforme vidéo pour les créateurs.\",\n yasakia: \"Rencontrez de vraies personnes : des présentations, et des photos vérifiées avant que vous les voyiez.\",\n yororis: \"Un espace d’écoute avec un visage et une voix.\",\n yomizaka: \"Yomizaka lit vos baux, contrats et factures.\",\n kachizaka: \"Les pronostics entre amis — des données en direct.\",\n akinaza: \"Tenir une boutique au Québec, pour moins cher — Loi 25 dès la conception.\",\n ui: \"Composants, jetons, icônes et graphiques — web et React Native.\",\n java: \"Des briques Spring Boot sur Maven Central.\",\n motion: \"Le labo du mouvement : le mouvement du kit, joué en direct, avec ses réglages.\",\n github: \"Tous les dépôts, au grand jour.\",\n },\n signature: { company: \"Une compagnie Krizaka\", product: \"Un produit Krizaka\", \"open-source\": \"Un projet open source de Krizaka\" },\n place: \"Fait à Montréal\",\n version: \"Version\",\n lang: \"fr\",\n company: { story: \"Notre histoire\", blog: \"Blogue\", contact: \"Nous écrire\" },\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sites/sites.ts"],"sourcesContent":["// Server-safe: plain data. The Krizaka family — every site Krizaka runs, once. The family switcher of the bar\n// (`NavBar.Family`) and the common footer (`SiteFooter`) both read it, so adding a site is one release of this package:\n// every bar and every footer of the family follow.\n//\n// The words of the family (a site's one line, the groups, the statuses, the footer's signature) are brand data, written\n// here in English and French (`familyLabels`) — a site passes the set of its locale, or its own translation.\n\n/** A site of the family. */\nexport type SiteId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"ui\" | \"java\" | \"ts\" | \"motion\" | \"github\";\n\n/** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */\nexport type SiteKind = \"company\" | \"product\" | \"open-source\";\n\n/**\n * `released`: open to visit. `upcoming`: announced, not released yet (said \"Coming soon\"). `construction`: under\n * construction — nothing offered (a QA-only app may run; the public page says so). `development`: being built in the\n * open, with a dev site (said \"In development\"). A site with none yet would use its repository as its address.\n */\nexport type SiteStatus = \"released\" | \"upcoming\" | \"construction\" | \"development\";\n\n/** The groups of the family, in reading order: Krizaka · Products · Open source · GitHub. */\nexport type SiteGroup = \"krizaka\" | \"products\" | \"open-source\" | \"github\";\n\n/** The mark a site wears: a brand of `ProductLogo` (Krizaka UI and Krizaka Java wear Krizaka's), or GitHub's. */\nexport type SiteMark = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"github\";\n\nexport interface Site {\n readonly id: SiteId;\n /** The name, never translated. */\n readonly name: string;\n /** The address of the site (its home). */\n readonly url: string;\n /** The address without the scheme, as people say it (`ui.krizaka.com`). */\n readonly host: string;\n readonly kind: SiteKind;\n readonly group: SiteGroup;\n readonly status: SiteStatus;\n readonly mark: SiteMark;\n /** The repository the site is built from. */\n readonly repo: string;\n}\n\nconst site = (s: Omit<Site, \"host\">): Site => ({ ...s, host: s.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\") });\n\n/** Every site of the family, in reading order (the group order, then the order inside each group). */\nexport const sites: readonly Site[] = [\n site({ id: \"krizaka\", name: \"Krizaka\", url: \"https://www.krizaka.com\", kind: \"company\", group: \"krizaka\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-com\" }),\n site({ id: \"orazaka\", name: \"Orazaka\", url: \"https://www.orazaka.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orazaka\", repo: \"https://github.com/krizaka/orazaka\" }),\n site({ id: \"orochia\", name: \"Orochia\", url: \"https://www.orochia.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orochia\", repo: \"https://github.com/krizaka/orochia\" }),\n site({ id: \"yasakia\", name: \"Yasakia\", url: \"https://www.yasakia.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yasakia\", repo: \"https://github.com/krizaka/yasakia\" }),\n site({ id: \"yororis\", name: \"Yororis\", url: \"https://www.yororis.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yororis\", repo: \"https://github.com/krizaka/yororis\" }),\n site({ id: \"yomizaka\", name: \"Yomizaka\", url: \"https://www.yomizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yomizaka\", repo: \"https://github.com/krizaka/yomizaka\" }),\n site({ id: \"kachizaka\", name: \"Kachizaka\", url: \"https://www.kachizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"kachizaka\", repo: \"https://github.com/krizaka/kachizaka\" }),\n site({ id: \"akinaza\", name: \"Akinaza\", url: \"https://www.akinaza.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"akinaza\", repo: \"https://github.com/krizaka/akinaza\" }),\n site({ id: \"ui\", name: \"Krizaka UI\", url: \"https://ui.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ui\" }),\n site({ id: \"java\", name: \"Krizaka Java\", url: \"https://java.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-build\" }),\n site({ id: \"ts\", name: \"Krizaka TS\", url: \"https://ts.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ts\" }),\n // The motion lab: built in the open on GitHub Pages; what proves itself there moves into Krizaka UI.\n site({ id: \"motion\", name: \"Krizaka Motion\", url: \"https://krizaka.github.io/krizaka-motion\", kind: \"open-source\", group: \"open-source\", status: \"development\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-motion\" }),\n site({ id: \"github\", name: \"GitHub\", url: \"https://github.com/krizaka\", kind: \"open-source\", group: \"github\", status: \"released\", mark: \"github\", repo: \"https://github.com/krizaka\" }),\n];\n\n/** The groups, in reading order. */\nexport const siteGroups: readonly SiteGroup[] = [\"krizaka\", \"products\", \"open-source\", \"github\"];\n\n/** A site by id. */\nexport function siteById(id: SiteId): Site {\n const found = sites.find((s) => s.id === id);\n if (!found) throw new Error(`Unknown Krizaka site \"${id}\".`);\n return found;\n}\n\n/** The family grouped, in reading order: `[{ group, sites }]`, empty groups left out. */\nexport function sitesByGroup(): { group: SiteGroup; sites: Site[] }[] {\n return siteGroups.map((group) => ({ group, sites: sites.filter((s) => s.group === group) })).filter((g) => g.sites.length > 0);\n}\n\n/** The languages the family's words are written in. */\nexport type SiteLocale = \"en\" | \"fr\";\n\n/** The company's own pages, the footer's « Krizaka » column (the brand's home is the signature line's link). */\nexport type CompanyLinkId = \"story\" | \"blog\" | \"contact\";\nexport const companyLinks: readonly { id: CompanyLinkId; url: Record<SiteLocale, string> }[] = [\n { id: \"story\", url: { en: \"https://www.krizaka.com/story\", fr: \"https://www.krizaka.com/fr/story\" } },\n { id: \"blog\", url: { en: \"https://blog.krizaka.com\", fr: \"https://blog.krizaka.com\" } },\n { id: \"contact\", url: { en: \"https://www.krizaka.com/contact\", fr: \"https://www.krizaka.com/fr/contact\" } },\n];\n\n/** The words of the family: the switcher, the footer, each site's one line. */\nexport interface FamilyLabels {\n /** The family's name: the switcher's navigation and its button, the footer's navigation (“Krizaka family”). */\n family: string;\n /** The heading of each group. */\n groups: Record<SiteGroup, string>;\n /** The badge of a site that is not released. */\n status: Record<Exclude<SiteStatus, \"released\">, string>;\n /** Says which site is the current one (“You are here”). */\n current: string;\n /** One line per site: what it is. */\n descriptions: Record<SiteId, string>;\n /** The footer's signature, by the kind of the current site (“A Krizaka product”). */\n signature: Record<SiteKind, string>;\n /** The footer's place line (“Made in Montréal”). */\n place: string;\n /** The accessible name of the version (“Version”). */\n version: string;\n /** The language of these words: the company links open in it. @default \"en\" */\n lang?: SiteLocale;\n /** The company's pages in the footer's « Krizaka » column. @default the English words */\n company?: Record<CompanyLinkId, string>;\n}\n\n/** The family's words in English and French. Pass `familyLabels[locale]`, or your own translation of the same shape. */\nexport const familyLabels: Record<SiteLocale, FamilyLabels> = {\n en: {\n family: \"Krizaka family\",\n groups: { krizaka: \"Krizaka\", products: \"Products\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Coming soon\", construction: \"Under construction\", development: \"In development\" },\n current: \"You are here\",\n descriptions: {\n krizaka: \"The company: our products, our open source, our story.\",\n orazaka: \"Sovereign AI on your own servers: chat, documents, images and video — and the engine under the other products.\",\n orochia: \"A video platform for creators.\",\n yasakia: \"Meet real people: introductions, and photos checked before you see them.\",\n yororis: \"A listening space with a face and a voice.\",\n yomizaka: \"Yomizaka reads your leases, contracts and invoices.\",\n kachizaka: \"Predictions among friends — live data, in real time.\",\n akinaza: \"Running a shop in Québec, for less — Loi 25 by design.\",\n ui: \"Components, tokens, icons and charts — web and React Native.\",\n java: \"Spring Boot building blocks on Maven Central.\",\n ts: \"TypeScript backend building blocks on npm.\",\n motion: \"The motion lab: the kit's motion, played live with its knobs.\",\n github: \"Every repository, in the open.\",\n },\n signature: { company: \"A Krizaka company\", product: \"A Krizaka product\", \"open-source\": \"A Krizaka open-source project\" },\n place: \"Made in Montréal\",\n version: \"Version\",\n lang: \"en\",\n company: { story: \"Our story\", blog: \"Blog\", contact: \"Contact us\" },\n },\n fr: {\n family: \"Famille Krizaka\",\n groups: { krizaka: \"Krizaka\", products: \"Produits\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Bientôt\", construction: \"En construction\", development: \"En développement\" },\n current: \"Vous êtes ici\",\n descriptions: {\n krizaka: \"La compagnie : nos produits, notre open source, notre histoire.\",\n orazaka: \"L’IA souveraine sur vos propres serveurs : conversation, documents, images et vidéo — et le moteur des autres produits.\",\n orochia: \"Une plateforme vidéo pour les créateurs.\",\n yasakia: \"Rencontrez de vraies personnes : des présentations, et des photos vérifiées avant que vous les voyiez.\",\n yororis: \"Un espace d’écoute avec un visage et une voix.\",\n yomizaka: \"Yomizaka lit vos baux, contrats et factures.\",\n kachizaka: \"Les pronostics entre amis — des données en direct.\",\n akinaza: \"Tenir une boutique au Québec, pour moins cher — Loi 25 dès la conception.\",\n ui: \"Composants, jetons, icônes et graphiques — web et React Native.\",\n java: \"Des briques Spring Boot sur Maven Central.\",\n ts: \"Des briques backend TypeScript sur npm.\",\n motion: \"Le labo du mouvement : le mouvement du kit, joué en direct, avec ses réglages.\",\n github: \"Tous les dépôts, au grand jour.\",\n },\n signature: { company: \"Une compagnie Krizaka\", product: \"Un produit Krizaka\", \"open-source\": \"Un projet open source de Krizaka\" },\n place: \"Fait à Montréal\",\n version: \"Version\",\n lang: \"fr\",\n company: { story: \"Notre histoire\", blog: \"Blogue\", contact: \"Nous écrire\" },\n },\n};\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.48.0",
3
+ "version": "3.50.0",
4
4
  "description": "The Krizaka front-end platform's components: the primitives (button, card, dialog, toast, menu, field, tabs, chip, switch, slider, command palette, theme, countdown…) on the semantic --kz-* tokens, the animated marks (Krizaka, Orazaka, Orochia, Yasakia, Yororis) and the motion signature — web and /native.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -769,7 +769,7 @@
769
769
  ],
770
770
  "peerDependencies": {
771
771
  "@krizaka/query": ">=0.2.1",
772
- "@krizaka/tailwind": ">=3.48.0",
772
+ "@krizaka/tailwind": ">=3.50.0",
773
773
  "@maplibre/maplibre-react-native": ">=11",
774
774
  "@shopify/flash-list": ">=2",
775
775
  "expo-camera": ">=16",
@@ -2099,7 +2099,7 @@
2099
2099
  {
2100
2100
  "name": "./settings-layout",
2101
2101
  "path": "dist/settings-layout.js",
2102
- "limit": "5 kB",
2102
+ "limit": "6 kB",
2103
2103
  "gzip": true,
2104
2104
  "ignore": [
2105
2105
  "tailwind-variants",
@@ -2600,8 +2600,8 @@
2600
2600
  "sonner": "^2.0.8",
2601
2601
  "tailwind-variants": "^3.3.1",
2602
2602
  "@krizaka/intl": "^0.2.1",
2603
- "@krizaka/tokens": "^3.48.0",
2604
- "@krizaka/pacer": "^0.1.0"
2603
+ "@krizaka/pacer": "^0.1.0",
2604
+ "@krizaka/tokens": "^3.50.0"
2605
2605
  },
2606
2606
  "scripts": {
2607
2607
  "build": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});for(const f of fs.readdirSync('registry/examples',{recursive:true}))if(/\\.(js|d\\.ts)$/.test(f))fs.rmSync('registry/examples/'+f)\" && tsup && node -e \"require('node:fs').copyFileSync('src/motion.css','dist/motion.css');require('node:fs').copyFileSync('src/blocks.css','dist/blocks.css');require('node:fs').copyFileSync('src/story.css','dist/story.css')\" && node scripts/build-registry.mjs",
@@ -141,7 +141,7 @@
141
141
  }
142
142
  ],
143
143
  "dependencies": [
144
- "@krizaka/tokens@^3.48.0"
144
+ "@krizaka/tokens@^3.50.0"
145
145
  ],
146
146
  "registryDependencies": [],
147
147
  "demo": {
@@ -0,0 +1,4 @@
1
+ import type { ComponentType } from "react";
2
+ /** A named example of the @krizaka/ui registry; `defaultOpen` opens what it opens (a dialog, a menu, toasts) at load. */
3
+ declare const Example: ComponentType<{ defaultOpen?: boolean }>;
4
+ export default Example;
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ // registry/examples/settings-layout/grouped.tsx
4
+ import { Avatar } from "@krizaka/ui/avatar";
5
+ import { Badge } from "@krizaka/ui/badge";
6
+ import { Button } from "@krizaka/ui/button";
7
+ import { SettingsLayout, SettingsRow, SettingsSection } from "@krizaka/ui/settings-layout";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ var NAV = [
10
+ {
11
+ id: "library",
12
+ label: "Library",
13
+ items: [
14
+ { id: "videos", label: "Videos", href: "#videos", current: true, badge: 12 },
15
+ { id: "collections", label: "Collections", href: "#collections", badge: 4 },
16
+ { id: "uploads", label: "Uploads", href: "#uploads" }
17
+ ]
18
+ },
19
+ {
20
+ id: "money",
21
+ label: "Money",
22
+ items: [
23
+ { id: "earnings", label: "Earnings", href: "#earnings" },
24
+ { id: "payouts", label: "Payouts", href: "#payouts" }
25
+ ]
26
+ },
27
+ {
28
+ id: "account",
29
+ label: "Account",
30
+ items: [
31
+ { id: "profile", label: "Profile", href: "#profile" },
32
+ { id: "notifications", label: "Notifications", href: "#notifications" },
33
+ { id: "security", label: "Security", href: "#security" }
34
+ ]
35
+ }
36
+ ];
37
+ function SettingsLayoutGrouped() {
38
+ return /* @__PURE__ */ jsx(
39
+ SettingsLayout,
40
+ {
41
+ className: "w-full",
42
+ navLabel: "Your space",
43
+ nav: NAV,
44
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [
45
+ /* @__PURE__ */ jsx(Avatar, { alt: "", size: "lg", fallback: "E" }),
46
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
47
+ /* @__PURE__ */ jsx("h1", { className: "truncate font-display text-xl font-extrabold tracking-tight text-fg", children: "Elena Varga" }),
48
+ /* @__PURE__ */ jsxs("p", { className: "font-mono text-xs text-fg-secondary", children: [
49
+ "@elena ",
50
+ /* @__PURE__ */ jsx(Badge, { tone: "accent", children: "Creator" })
51
+ ] })
52
+ ] }),
53
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", children: "View public page" })
54
+ ] }),
55
+ children: /* @__PURE__ */ jsxs(SettingsSection, { variant: "flat", id: "videos", title: "Videos", description: "What you published and what is still processing.", children: [
56
+ /* @__PURE__ */ jsx(SettingsRow, { label: "Published", children: "12" }),
57
+ /* @__PURE__ */ jsx(SettingsRow, { label: "Processing", description: "They appear here when they are ready.", children: "0" })
58
+ ] })
59
+ }
60
+ );
61
+ }
62
+ export {
63
+ SettingsLayoutGrouped as default
64
+ };
@@ -0,0 +1,66 @@
1
+ import { Avatar } from "@krizaka/ui/avatar";
2
+ import { Badge } from "@krizaka/ui/badge";
3
+ import { Button } from "@krizaka/ui/button";
4
+ import { SettingsLayout, SettingsRow, SettingsSection } from "@krizaka/ui/settings-layout";
5
+
6
+ const NAV = [
7
+ {
8
+ id: "library",
9
+ label: "Library",
10
+ items: [
11
+ { id: "videos", label: "Videos", href: "#videos", current: true, badge: 12 },
12
+ { id: "collections", label: "Collections", href: "#collections", badge: 4 },
13
+ { id: "uploads", label: "Uploads", href: "#uploads" },
14
+ ],
15
+ },
16
+ {
17
+ id: "money",
18
+ label: "Money",
19
+ items: [
20
+ { id: "earnings", label: "Earnings", href: "#earnings" },
21
+ { id: "payouts", label: "Payouts", href: "#payouts" },
22
+ ],
23
+ },
24
+ {
25
+ id: "account",
26
+ label: "Account",
27
+ items: [
28
+ { id: "profile", label: "Profile", href: "#profile" },
29
+ { id: "notifications", label: "Notifications", href: "#notifications" },
30
+ { id: "security", label: "Security", href: "#security" },
31
+ ],
32
+ },
33
+ ] as const;
34
+
35
+ // TEMPLATE — a private space with more than five sections: who you are on top, the sections grouped in the sidebar,
36
+ // one menu on a phone (a strip of eight chips is a puzzle).
37
+ export default function SettingsLayoutGrouped() {
38
+ return (
39
+ <SettingsLayout
40
+ className="w-full"
41
+ navLabel="Your space"
42
+ nav={NAV}
43
+ header={
44
+ <>
45
+ <Avatar alt="" size="lg" fallback="E" />
46
+ <div className="min-w-0 flex-1">
47
+ <h1 className="truncate font-display text-xl font-extrabold tracking-tight text-fg">Elena Varga</h1>
48
+ <p className="font-mono text-xs text-fg-secondary">
49
+ @elena <Badge tone="accent">Creator</Badge>
50
+ </p>
51
+ </div>
52
+ <Button size="sm" variant="ghost">
53
+ View public page
54
+ </Button>
55
+ </>
56
+ }
57
+ >
58
+ <SettingsSection variant="flat" id="videos" title="Videos" description="What you published and what is still processing.">
59
+ <SettingsRow label="Published">12</SettingsRow>
60
+ <SettingsRow label="Processing" description="They appear here when they are ready.">
61
+ 0
62
+ </SettingsRow>
63
+ </SettingsSection>
64
+ </SettingsLayout>
65
+ );
66
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.48.0",
3
+ "version": "3.50.0",
4
4
  "templatePurposes": {
5
5
  "convince": {
6
6
  "title": "Convince",
@@ -365,6 +365,15 @@
365
365
  "purpose": "operate",
366
366
  "why": "A person scans the settings, finds the one to change and changes it in place, without cards competing for attention."
367
367
  },
368
+ {
369
+ "component": "settings-layout",
370
+ "platform": "web",
371
+ "name": "grouped",
372
+ "title": "A private space",
373
+ "description": "Who you are on top, eight sections in three groups with counts; one menu on a phone.",
374
+ "purpose": "operate",
375
+ "why": "A person with many places to go finds each under its group, and never meets a strip of chips that hides half of them."
376
+ },
368
377
  {
369
378
  "component": "screen",
370
379
  "platform": "native",
@@ -2255,7 +2264,7 @@
2255
2264
  "native": 0
2256
2265
  },
2257
2266
  "dependencies": [
2258
- "@krizaka/tokens@^3.48.0"
2267
+ "@krizaka/tokens@^3.50.0"
2259
2268
  ],
2260
2269
  "registryDependencies": []
2261
2270
  },
@@ -3282,7 +3291,7 @@
3282
3291
  "native": 2
3283
3292
  },
3284
3293
  "dependencies": [
3285
- "@krizaka/tokens@^3.48.0",
3294
+ "@krizaka/tokens@^3.50.0",
3286
3295
  "@protomaps/basemaps@5.7.2",
3287
3296
  "tailwind-variants@^3.3.1"
3288
3297
  ],
@@ -3721,7 +3730,7 @@
3721
3730
  "native": 0
3722
3731
  },
3723
3732
  "dependencies": [
3724
- "@krizaka/tokens@^3.48.0",
3733
+ "@krizaka/tokens@^3.50.0",
3725
3734
  "radix-ui@^1.7.0",
3726
3735
  "tailwind-variants@^3.3.1"
3727
3736
  ],
@@ -4386,7 +4395,7 @@
4386
4395
  "native": 0
4387
4396
  },
4388
4397
  "dependencies": [
4389
- "@krizaka/tokens@^3.48.0",
4398
+ "@krizaka/tokens@^3.50.0",
4390
4399
  "radix-ui@^1.7.0",
4391
4400
  "tailwind-variants@^3.3.1"
4392
4401
  ],
@@ -4676,8 +4685,8 @@
4676
4685
  "name": "settings-layout",
4677
4686
  "type": "primitive",
4678
4687
  "title": "Settings layout",
4679
- "summary": "Settings: the sections on the side (a row of chips that scrolls on a phone) and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
4680
- "description": "Settings: the sections on the side (a row of chips that scrolls on a phone) and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
4688
+ "summary": "Settings: the sections on the side, alone or in titled groups (a row of chips on a phone, one menu past five), an optional identity header and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
4689
+ "description": "Settings: the sections on the side, alone or in titled groups (a row of chips on a phone, one menu past five), an optional identity header and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
4681
4690
  "status": "beta",
4682
4691
  "category": "layouts",
4683
4692
  "platforms": "both",
@@ -4693,7 +4702,7 @@
4693
4702
  }
4694
4703
  },
4695
4704
  "examples": {
4696
- "web": 3,
4705
+ "web": 4,
4697
4706
  "native": 1
4698
4707
  },
4699
4708
  "dependencies": [
@@ -4759,7 +4768,7 @@
4759
4768
  "native": 0
4760
4769
  },
4761
4770
  "dependencies": [
4762
- "@krizaka/tokens@^3.48.0",
4771
+ "@krizaka/tokens@^3.50.0",
4763
4772
  "radix-ui@^1.7.0",
4764
4773
  "tailwind-variants@^3.3.1"
4765
4774
  ],
package/registry/map.json CHANGED
@@ -1074,7 +1074,7 @@
1074
1074
  }
1075
1075
  ],
1076
1076
  "dependencies": [
1077
- "@krizaka/tokens@^3.48.0",
1077
+ "@krizaka/tokens@^3.50.0",
1078
1078
  "@protomaps/basemaps@5.7.2",
1079
1079
  "tailwind-variants@^3.3.1"
1080
1080
  ],
@@ -664,7 +664,7 @@
664
664
  }
665
665
  ],
666
666
  "dependencies": [
667
- "@krizaka/tokens@^3.48.0",
667
+ "@krizaka/tokens@^3.50.0",
668
668
  "radix-ui@^1.7.0",
669
669
  "tailwind-variants@^3.3.1"
670
670
  ],
@@ -342,7 +342,7 @@
342
342
  }
343
343
  ],
344
344
  "dependencies": [
345
- "@krizaka/tokens@^3.48.0",
345
+ "@krizaka/tokens@^3.50.0",
346
346
  "radix-ui@^1.7.0",
347
347
  "tailwind-variants@^3.3.1"
348
348
  ],
@@ -2,8 +2,8 @@
2
2
  "name": "settings-layout",
3
3
  "type": "primitive",
4
4
  "title": "Settings layout",
5
- "summary": "Settings: the sections on the side (a row of chips that scrolls on a phone) and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
6
- "description": "Settings: the sections on the side (a row of chips that scrolls on a phone) and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
5
+ "summary": "Settings: the sections on the side, alone or in titled groups (a row of chips on a phone, one menu past five), an optional identity header and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
6
+ "description": "Settings: the sections on the side, alone or in titled groups (a row of chips on a phone, one menu past five), an optional identity header and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).",
7
7
  "status": "beta",
8
8
  "deprecated": null,
9
9
  "category": "layouts",
@@ -34,6 +34,7 @@
34
34
  }
35
35
  ],
36
36
  "bestPractices": [
37
+ "Past five sections, group them (`nav` as `SettingsNavGroup[]`): the sidebar shows the group titles and a phone gets one menu, never a scrolling strip of chips.",
37
38
  "Prefer `variant=\"flat\"` and `SettingsRow` for a product's settings: a label and its help on the left, the control on the right, no card.",
38
39
  "Keep support-only facts (an account id) in a last `Advanced` section, small and mono, never in the first screen.",
39
40
  "One save per section, at its foot: a change never waits for another.",
@@ -61,15 +62,20 @@
61
62
  "description": "How long « Saved » stays before the section goes quiet again.",
62
63
  "props": []
63
64
  },
65
+ {
66
+ "component": "SETTINGS_CHIPS_MAX",
67
+ "description": "Past this many sections a phone gets one menu instead of a strip of chips (« never more than about 5 items in a horizontal tab bar »).",
68
+ "props": []
69
+ },
64
70
  {
65
71
  "component": "SettingsLayout",
66
- "description": "A settings screen: sections on the left (chips on a phone), forms on the right.",
72
+ "description": "A settings screen: sections on the left (chips on a phone, one menu past five), forms on the right.",
67
73
  "props": [
68
74
  {
69
75
  "name": "nav",
70
- "type": "readonly SettingsNavItem[]",
76
+ "type": "readonly SettingsNavItem[] | readonly SettingsNavGroup[]",
71
77
  "default": null,
72
- "description": "The sections.",
78
+ "description": "The sections, alone or in titled groups.",
73
79
  "required": true
74
80
  },
75
81
  {
@@ -86,6 +92,13 @@
86
92
  "description": "One sentence under it.",
87
93
  "required": false
88
94
  },
95
+ {
96
+ "name": "header",
97
+ "type": "ReactNode",
98
+ "default": null,
99
+ "description": "What the screen is about, above the sections: who the account is, with their actions. Replaces `title` and `description`.",
100
+ "required": false
101
+ },
89
102
  {
90
103
  "name": "linkAs",
91
104
  "type": "ElementType<any, keyof IntrinsicElements>",
@@ -201,6 +214,17 @@
201
214
  "module": "@krizaka/ui/registry/examples/settings-layout/flat",
202
215
  "code": "import { Badge } from \"@krizaka/ui/badge\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { Input } from \"@krizaka/ui/field\";\nimport { SettingsLayout, SettingsRow, SettingsSection } from \"@krizaka/ui/settings-layout\";\n\nconst NAV = [\n { id: \"account\", label: \"Account\", href: \"/settings/account\", current: true },\n { id: \"appearance\", label: \"Appearance\", href: \"/settings/appearance\" },\n { id: \"integrations\", label: \"Integrations\", href: \"/settings/integrations\" },\n];\n\n// TEMPLATE — calm settings: sections on the left, one column of rows (label and help left, control right) divided by hairlines.\nexport default function SettingsLayoutFlat() {\n return (\n <SettingsLayout className=\"w-full\" variant=\"flat\" title=\"Settings\" nav={NAV} navLabel=\"Settings sections\">\n <SettingsSection variant=\"flat\" id=\"account\" title=\"Account\" description=\"Who you are in this workspace.\">\n <SettingsRow label=\"Username\" description=\"Shown to your team.\" htmlFor=\"flat-username\">\n <Input id=\"flat-username\" defaultValue=\"oussama\" className=\"w-56\" />\n </SettingsRow>\n <SettingsRow label=\"Email\">oussama@krizaka.com</SettingsRow>\n <SettingsRow label=\"Role\"><Badge>Admin</Badge></SettingsRow>\n </SettingsSection>\n <SettingsSection variant=\"flat\" title=\"Danger zone\">\n <SettingsRow label=\"Sign out everywhere\" description=\"Ends every session on every device.\">\n <Button size=\"sm\" variant=\"secondary\">Sign out</Button>\n </SettingsRow>\n </SettingsSection>\n </SettingsLayout>\n );\n}\n"
203
216
  },
217
+ {
218
+ "name": "grouped",
219
+ "title": "A private space",
220
+ "description": "Who you are on top, eight sections in three groups with counts; one menu on a phone.",
221
+ "template": true,
222
+ "purpose": "operate",
223
+ "why": "A person with many places to go finds each under its group, and never meets a strip of chips that hides half of them.",
224
+ "path": "examples/settings-layout/grouped.tsx",
225
+ "module": "@krizaka/ui/registry/examples/settings-layout/grouped",
226
+ "code": "import { Avatar } from \"@krizaka/ui/avatar\";\nimport { Badge } from \"@krizaka/ui/badge\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { SettingsLayout, SettingsRow, SettingsSection } from \"@krizaka/ui/settings-layout\";\n\nconst NAV = [\n {\n id: \"library\",\n label: \"Library\",\n items: [\n { id: \"videos\", label: \"Videos\", href: \"#videos\", current: true, badge: 12 },\n { id: \"collections\", label: \"Collections\", href: \"#collections\", badge: 4 },\n { id: \"uploads\", label: \"Uploads\", href: \"#uploads\" },\n ],\n },\n {\n id: \"money\",\n label: \"Money\",\n items: [\n { id: \"earnings\", label: \"Earnings\", href: \"#earnings\" },\n { id: \"payouts\", label: \"Payouts\", href: \"#payouts\" },\n ],\n },\n {\n id: \"account\",\n label: \"Account\",\n items: [\n { id: \"profile\", label: \"Profile\", href: \"#profile\" },\n { id: \"notifications\", label: \"Notifications\", href: \"#notifications\" },\n { id: \"security\", label: \"Security\", href: \"#security\" },\n ],\n },\n] as const;\n\n// TEMPLATE — a private space with more than five sections: who you are on top, the sections grouped in the sidebar,\n// one menu on a phone (a strip of eight chips is a puzzle).\nexport default function SettingsLayoutGrouped() {\n return (\n <SettingsLayout\n className=\"w-full\"\n navLabel=\"Your space\"\n nav={NAV}\n header={\n <>\n <Avatar alt=\"\" size=\"lg\" fallback=\"E\" />\n <div className=\"min-w-0 flex-1\">\n <h1 className=\"truncate font-display text-xl font-extrabold tracking-tight text-fg\">Elena Varga</h1>\n <p className=\"font-mono text-xs text-fg-secondary\">\n @elena <Badge tone=\"accent\">Creator</Badge>\n </p>\n </div>\n <Button size=\"sm\" variant=\"ghost\">\n View public page\n </Button>\n </>\n }\n >\n <SettingsSection variant=\"flat\" id=\"videos\" title=\"Videos\" description=\"What you published and what is still processing.\">\n <SettingsRow label=\"Published\">12</SettingsRow>\n <SettingsRow label=\"Processing\" description=\"They appear here when they are ready.\">\n 0\n </SettingsRow>\n </SettingsSection>\n </SettingsLayout>\n );\n}\n"
227
+ },
204
228
  {
205
229
  "name": "sections",
206
230
  "title": "One section per page",
@@ -324,11 +348,11 @@
324
348
  },
325
349
  {
326
350
  "path": "settings-layout/index.ts",
327
- "content": "/** @krizaka/ui/settings-layout — `SettingsLayout`, `SettingsSection` and `SettingsRow`: sections on the side, forms as titled cards or flat rows. Server-safe. */\nexport { SAVED_VISIBLE_MS, SaveStatus, type SaveTracker, useSaveStatus } from \"./save-status\";\nexport { SettingsLayout, settingsLayout, type SettingsLayoutProps, type SettingsNavItem, SettingsRow, type SettingsRowProps, SettingsSection, type SettingsSectionProps } from \"./settings-layout\";\n"
351
+ "content": "/** @krizaka/ui/settings-layout — `SettingsLayout`, `SettingsSection` and `SettingsRow`: sections on the side (grouped, one menu on a phone past five), forms as titled cards or flat rows. Server-safe. */\nexport { SAVED_VISIBLE_MS, SaveStatus, type SaveTracker, useSaveStatus } from \"./save-status\";\nexport { SETTINGS_CHIPS_MAX, SettingsLayout, settingsLayout, type SettingsLayoutProps, type SettingsNavGroup, type SettingsNavItem, SettingsRow, type SettingsRowProps, SettingsSection, type SettingsSectionProps } from \"./settings-layout\";\n"
328
352
  },
329
353
  {
330
354
  "path": "settings-layout/meta.ts",
331
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Settings layout\",\n summary: \"Settings: the sections on the side (a row of chips that scrolls on a phone) and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).\",\n status: \"beta\",\n category: \"layouts\",\n platforms: \"both\",\n parity: { desktop: \"web\" },\n whenToUse: [\"Account, workspace and administration settings.\", \"Any long form that splits into independent groups.\"],\n whenNotToUse: [\n { when: \"For a step-by-step setup the user follows once.\", use: \"stepper\" },\n { when: \"For one small form.\", use: \"field\" },\n ],\n bestPractices: [\"Prefer `variant=\\\"flat\\\"` and `SettingsRow` for a product's settings: a label and its help on the left, the control on the right, no card.\", \"Keep support-only facts (an account id) in a last `Advanced` section, small and mono, never in the first screen.\", \"One save per section, at its foot: a change never waits for another.\", \"Sections as pages (`/settings/billing`) or as anchors of one long page (`#billing`).\", \"Say in the description what the section changes for the user.\"],\n accessibility: {\n keyboard: [],\n notes: [\"The sections are a `nav` named by `navLabel`; the current one has `aria-current` (page or location).\", \"Each section is a `section` named by its `h2`.\"],\n },\n related: [\"field\", \"switch\", \"app-shell\"],\n native: {\n imports: [\"SettingsGroup\", \"SettingsRow\"],\n differences: [\n \"Only the rows: `SettingsGroup` (a titled card of hairline-divided rows) and `SettingsRow` (icon tile, label, help, value or control, a chevron when `onPress` opens something). The sections-on-the-side layout is a web idea; on a phone each section is a screen.\",\n \"A row with `onPress` is a button named by its visible words; a row with a `trailing` control (a `Switch`) leaves the control as the thing to press.\",\n ],\n examples: [{ name: \"account\", title: \"Account settings\", description: \"A group of rows: a value, a link that opens a screen, a switch, a destructive action.\" }],\n },\n web: {\n imports: [\"SettingsLayout\", \"SettingsSection\", \"SettingsRow\"],\n examples: [\n { name: \"account\", title: \"Account settings\", description: \"Profile and notifications, each with its own save; the sections as chips on a phone.\", template: true, purpose: \"operate\", why: \"A person changes one setting at a time, saves it at once and knows where each lives.\" },\n { name: \"flat\", title: \"Calm settings\", description: \"The flat variant: one column of at most 45rem, each setting a row (label and help left, control right) divided by hairlines.\", template: true, purpose: \"operate\", why: \"A person scans the settings, finds the one to change and changes it in place, without cards competing for attention.\" },\n { name: \"sections\", title: \"One section per page\", description: \"A workspace's settings, the current section marked.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
355
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Settings layout\",\n summary: \"Settings: the sections on the side, alone or in titled groups (a row of chips on a phone, one menu past five), an optional identity header and the current section's forms, as titled cards with their own save or, flat, as hairline-divided rows (`SettingsRow`).\",\n status: \"beta\",\n category: \"layouts\",\n platforms: \"both\",\n parity: { desktop: \"web\" },\n whenToUse: [\"Account, workspace and administration settings.\", \"Any long form that splits into independent groups.\"],\n whenNotToUse: [\n { when: \"For a step-by-step setup the user follows once.\", use: \"stepper\" },\n { when: \"For one small form.\", use: \"field\" },\n ],\n bestPractices: [\"Past five sections, group them (`nav` as `SettingsNavGroup[]`): the sidebar shows the group titles and a phone gets one menu, never a scrolling strip of chips.\", \"Prefer `variant=\\\"flat\\\"` and `SettingsRow` for a product's settings: a label and its help on the left, the control on the right, no card.\", \"Keep support-only facts (an account id) in a last `Advanced` section, small and mono, never in the first screen.\", \"One save per section, at its foot: a change never waits for another.\", \"Sections as pages (`/settings/billing`) or as anchors of one long page (`#billing`).\", \"Say in the description what the section changes for the user.\"],\n accessibility: {\n keyboard: [],\n notes: [\"The sections are a `nav` named by `navLabel`; the current one has `aria-current` (page or location).\", \"Each section is a `section` named by its `h2`.\"],\n },\n related: [\"field\", \"switch\", \"app-shell\"],\n native: {\n imports: [\"SettingsGroup\", \"SettingsRow\"],\n differences: [\n \"Only the rows: `SettingsGroup` (a titled card of hairline-divided rows) and `SettingsRow` (icon tile, label, help, value or control, a chevron when `onPress` opens something). The sections-on-the-side layout is a web idea; on a phone each section is a screen.\",\n \"A row with `onPress` is a button named by its visible words; a row with a `trailing` control (a `Switch`) leaves the control as the thing to press.\",\n ],\n examples: [{ name: \"account\", title: \"Account settings\", description: \"A group of rows: a value, a link that opens a screen, a switch, a destructive action.\" }],\n },\n web: {\n imports: [\"SettingsLayout\", \"SettingsSection\", \"SettingsRow\"],\n examples: [\n { name: \"account\", title: \"Account settings\", description: \"Profile and notifications, each with its own save; the sections as chips on a phone.\", template: true, purpose: \"operate\", why: \"A person changes one setting at a time, saves it at once and knows where each lives.\" },\n { name: \"flat\", title: \"Calm settings\", description: \"The flat variant: one column of at most 45rem, each setting a row (label and help left, control right) divided by hairlines.\", template: true, purpose: \"operate\", why: \"A person scans the settings, finds the one to change and changes it in place, without cards competing for attention.\" },\n { name: \"grouped\", title: \"A private space\", description: \"Who you are on top, eight sections in three groups with counts; one menu on a phone.\", template: true, purpose: \"operate\", why: \"A person with many places to go finds each under its group, and never meets a strip of chips that hides half of them.\" },\n { name: \"sections\", title: \"One section per page\", description: \"A workspace's settings, the current section marked.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
332
356
  },
333
357
  {
334
358
  "path": "settings-layout/save-status.ts",
@@ -336,7 +360,7 @@
336
360
  },
337
361
  {
338
362
  "path": "settings-layout/settings-layout.tsx",
339
- "content": "// Server-safe. Settings, read from the products' account and administration screens: a list of sections on the left\n// (a row of chips that scrolls on a phone), the current section's forms on the right, each form a titled card with its\n// own save at the foot. The sections are links (one page each) or anchors (one long page).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Button } from \"../button/button\";\nimport { SaveStatus } from \"./save-status\";\n\nexport const settingsLayout = tv({\n slots: {\n root: \"flex w-full flex-col gap-6\",\n head: \"flex flex-col gap-1\",\n title: \"font-display text-2xl font-extrabold tracking-tight text-fg\",\n description: \"max-w-2xl text-sm text-fg-secondary\",\n columns: \"grid grid-cols-1 gap-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10\",\n nav: \"-mx-4 overflow-x-auto overflow-y-hidden px-4 py-1 -my-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden lg:mx-0 lg:overflow-visible lg:px-0 lg:py-0 lg:my-0\",\n navList: \"flex w-max gap-2 lg:sticky lg:top-20 lg:w-auto lg:flex-col lg:gap-0.5\",\n navLink:\n \"flex h-9 items-center gap-2.5 whitespace-nowrap rounded-sm border border-border-default bg-surface-1 px-3.5 text-sm font-medium text-fg-secondary no-underline \" +\n \"hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current]:border-accent aria-[current]:text-fg \" +\n \"lg:rounded-lg lg:border-transparent lg:bg-transparent lg:px-3 lg:hover:bg-surface-2 lg:aria-[current]:bg-accent-soft [&>svg]:h-4 [&>svg]:w-4\",\n content: \"flex min-w-0 flex-col gap-6\",\n section: \"rounded-xl border border-border-default bg-surface-1\",\n sectionHead: \"flex flex-col gap-1 px-5 pt-5\",\n sectionTitle: \"text-base font-bold text-fg\",\n sectionDescription: \"text-sm text-fg-secondary\",\n sectionBody: \"flex flex-col gap-4 px-5 py-5\",\n sectionFooter: \"flex flex-wrap items-center justify-end gap-2 border-t border-border-subtle bg-surface-2/40 px-5 py-3 rounded-b-xl\",\n // The flat look: no card, hairline-divided rows (Linear / Vercel / GitHub settings).\n rows: \"flex flex-col divide-y divide-border-subtle border-y border-border-subtle\",\n row: \"flex flex-col gap-2 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6\",\n rowText: \"flex min-w-0 flex-col gap-0.5\",\n rowLabel: \"text-sm font-medium text-fg\",\n rowDescription: \"text-sm text-fg-secondary\",\n rowControl: \"flex min-w-0 shrink-0 items-center gap-2 text-sm text-fg sm:max-w-[60%] sm:justify-end\",\n },\n variants: {\n variant: {\n card: {},\n flat: {\n content: \"max-w-[45rem] gap-10\",\n section: \"rounded-none border-0 bg-transparent\",\n sectionHead: \"gap-0.5 px-0 pb-3 pt-0\",\n sectionBody: \"gap-0 px-0 py-0\",\n sectionFooter: \"justify-start border-t-0 bg-transparent px-0 pb-0 pt-4\",\n },\n },\n },\n defaultVariants: { variant: \"card\" },\n});\n\nexport interface SettingsNavItem {\n /** Its id (the anchor of a section on one long page). */\n id: string;\n /** Its words. */\n label: string;\n /** Where it leads: a page (`/settings/billing`) or an anchor (`#billing`). */\n href: string;\n /** Its icon (a signature icon). */\n icon?: React.ReactNode;\n /** The section shown now: `aria-current`. */\n current?: boolean;\n}\n\nexport type SettingsLayoutProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The screen's title (`Settings`). */\n title?: React.ReactNode;\n /** One sentence under it. */\n description?: React.ReactNode;\n /** The sections. */\n nav: readonly SettingsNavItem[];\n /** The navigation's accessible name (`Settings sections`). */\n navLabel: string;\n /** The app's link component for the sections (Next's `Link`); an `<a>` otherwise. */\n linkAs?: React.ElementType;\n /**\n * `card` (each form a titled card) or `flat` (one calm column of at most 45rem, rows divided by hairlines).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The current section's forms (`SettingsSection`). */\n children: React.ReactNode;\n};\n\n/** A settings screen: sections on the left (chips on a phone), forms on the right. */\nexport function SettingsLayout({ title, description, nav, navLabel, linkAs: Link = \"a\", variant = \"card\", className, children, ...props }: SettingsLayoutProps) {\n const s = settingsLayout({ variant });\n return (\n <div className={s.root({ className })} {...props}>\n {(title || description) && (\n <div className={s.head()}>\n {title && <h1 className={s.title()}>{title}</h1>}\n {description && <p className={s.description()}>{description}</p>}\n </div>\n )}\n <div className={s.columns()}>\n <nav aria-label={navLabel} className={s.nav()}>\n <ul className={s.navList()}>\n {nav.map((item) => (\n <li key={item.id}>\n <Link href={item.href} className={s.navLink()} aria-current={item.current ? (item.href.startsWith(\"#\") ? \"location\" : \"page\") : undefined}>\n {item.icon}\n {item.label}\n </Link>\n </li>\n ))}\n </ul>\n </nav>\n <div className={s.content()}>{children}</div>\n </div>\n </div>\n );\n}\n\nexport interface SectionSaveLabels {\n /** “Save”. */\n save: string;\n /** “Saving…”. */\n saving: string;\n /** “Saved”. */\n saved: string;\n /** What failed, one sentence (“Could not save. Try again.”). */\n error: string;\n /** “Undo” — only shown after a save, when `onUndo` is given. */\n undo: string;\n /** “Discard changes” — only shown while dirty, when `onDiscard` is given. */\n discard: string;\n}\n\n/** The one save of a section: its dirty state, its status, its words. */\nexport interface SectionSave {\n /** Whether a field differs from what is saved: the save is enabled only then. */\n dirty: boolean;\n /** Where the save is (`useSaveStatus().status`). */\n status: SaveStatus;\n /** Saves the section (the form's submit, Enter included). */\n onSave: () => void;\n /** Puts the previous values back after a save, optimistic update included. */\n onUndo?: () => void;\n /** Throws the edits away while dirty. */\n onDiscard?: () => void;\n labels: SectionSaveLabels;\n}\n\nexport type SettingsSectionProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n /**\n * One save for the whole section: the section becomes a form, the foot shows the Save (enabled while dirty), the\n * result in a live region (saved, or the error as an alert) and an undo after a save. Replaces `footer`.\n */\n save?: SectionSave;\n /** The section's title: it names the region (and is its anchor's target when `id` is set). */\n title: React.ReactNode;\n /** What these settings change, in one sentence. */\n description?: React.ReactNode;\n /** At the foot: the section's own save (a Button), a note. */\n footer?: React.ReactNode;\n /**\n * `card` (a titled card) or `flat` (no card: a title, then hairline-divided `SettingsRow`s).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The fields (or, flat, the `SettingsRow`s). A section of a title, a sentence and a foot action has none. */\n children?: React.ReactNode;\n};\n\n/** A group of settings: a titled card with its fields and its own save, or flat rows under a title. */\nexport function SettingsSection({ title, description, footer, save, variant = \"card\", id, className, children, ...props }: SettingsSectionProps) {\n const s = settingsLayout({ variant });\n const auto = React.useId();\n const titleId = `${id ?? auto}-title`;\n const head = (\n <div className={s.sectionHead()}>\n <h2 id={titleId} className={s.sectionTitle()}>\n {title}\n </h2>\n {description && <p className={s.sectionDescription()}>{description}</p>}\n </div>\n );\n const body = children === undefined ? null : (\n <div className={variant === \"flat\" ? s.rows() : s.sectionBody()} data-settings-rows={variant === \"flat\" ? \"\" : undefined}>\n {children}\n </div>\n );\n return (\n <section id={id} aria-labelledby={titleId} data-dirty={save?.dirty ? \"\" : undefined} className={s.section({ className })} {...props}>\n {save ? (\n <form\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n if (save.dirty && save.status !== SaveStatus.Saving) save.onSave();\n }}\n >\n {head}\n {body}\n <div className={s.sectionFooter()}>\n <SaveFeedback save={save} />\n {save.onDiscard && save.dirty ? (\n <Button type=\"button\" variant=\"ghost\" onClick={save.onDiscard}>\n {save.labels.discard}\n </Button>\n ) : null}\n <Button type=\"submit\" variant=\"primary\" disabled={!save.dirty || save.status === SaveStatus.Saving} loading={save.status === SaveStatus.Saving}>\n {save.labels.save}\n </Button>\n </div>\n </form>\n ) : (\n <>\n {head}\n {body}\n {footer && <div className={s.sectionFooter()}>{footer}</div>}\n </>\n )}\n </section>\n );\n}\n\n/** The result of the last save: « Saved » (with Undo) in a polite live region, a failure as an alert. The region stays mounted so the change is announced. */\nfunction SaveFeedback({ save }: { save: SectionSave }) {\n const failed = save.status === SaveStatus.Error;\n const saved = save.status === SaveStatus.Saved;\n return (\n <div className=\"mr-auto flex min-w-0 items-center gap-2 text-sm\">\n <span role={failed ? \"alert\" : \"status\"} aria-live={failed ? \"assertive\" : \"polite\"} className={failed ? \"text-danger-text\" : \"text-fg-secondary\"}>\n {failed ? save.labels.error : saved ? save.labels.saved : save.status === SaveStatus.Saving ? save.labels.saving : \"\"}\n </span>\n {saved && save.onUndo ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={save.onUndo}>\n {save.labels.undo}\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport type SettingsRowProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** What the setting is (`Username`). */\n label: React.ReactNode;\n /** The help text under it, one sentence. */\n description?: React.ReactNode;\n /** The id of the control, so the label names it (a `<label htmlFor>`). Leave out for a plain value. */\n htmlFor?: string;\n /** The control or the value, on the right (stacked under the label on a phone). */\n children?: React.ReactNode;\n};\n\n/** One setting: its label and help on the left, its control or value on the right. Put rows in a flat `SettingsSection`. */\nexport function SettingsRow({ label, description, htmlFor, className, children, ...props }: SettingsRowProps) {\n const s = settingsLayout();\n const Label = htmlFor ? \"label\" : \"span\";\n return (\n <div className={s.row({ className })} {...props}>\n <div className={s.rowText()}>\n <Label className={s.rowLabel()} {...(htmlFor ? { htmlFor } : {})}>\n {label}\n </Label>\n {description && <span className={s.rowDescription()}>{description}</span>}\n </div>\n {children !== undefined && <div className={s.rowControl()}>{children}</div>}\n </div>\n );\n}\n"
363
+ "content": "// Server-safe. Settings, read from the products' account and administration screens: a list of sections on the left\n// (a row of chips that scrolls on a phone, a disclosure menu past five), the current section's forms on the right, each\n// form a titled card with its own save at the foot. The sections are links (one page each) or anchors (one long page),\n// alone or in titled groups.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Button } from \"../button/button\";\nimport { SaveStatus } from \"./save-status\";\n\nexport const settingsLayout = tv({\n slots: {\n root: \"flex w-full flex-col gap-6\",\n head: \"flex flex-col gap-1\",\n title: \"font-display text-2xl font-extrabold tracking-tight text-fg\",\n description: \"max-w-2xl text-sm text-fg-secondary\",\n columns: \"grid grid-cols-1 gap-6 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10\",\n nav: \"-mx-4 overflow-x-auto overflow-y-hidden px-4 py-1 -my-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden lg:mx-0 lg:overflow-visible lg:px-0 lg:py-0 lg:my-0\",\n navList: \"flex w-max gap-2 lg:sticky lg:top-20 lg:w-auto lg:flex-col lg:gap-0.5\",\n navLink:\n \"flex h-9 items-center gap-2.5 whitespace-nowrap rounded-sm border border-border-default bg-surface-1 px-3.5 text-sm font-medium text-fg-secondary no-underline \" +\n \"hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current]:border-accent aria-[current]:text-fg \" +\n \"lg:rounded-lg lg:border-transparent lg:bg-transparent lg:px-3 lg:hover:bg-surface-2 lg:aria-[current]:bg-accent-soft [&>svg]:h-4 [&>svg]:w-4\",\n // Past five sections a strip of chips is a puzzle: the sidebar groups them, a phone gets one menu (a disclosure: no script).\n sidebar: \"hidden lg:block\",\n sidebarList: \"flex flex-col gap-6 lg:sticky lg:top-20\",\n group: \"flex flex-col gap-1\",\n groupLabel: \"px-3 text-xs font-semibold text-fg-secondary\",\n groupLink:\n \"flex h-9 items-center gap-2.5 whitespace-nowrap rounded-lg px-3 text-sm font-medium text-fg-secondary no-underline hover:bg-surface-2 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current]:bg-accent-soft aria-[current]:text-fg [&>svg]:h-4 [&>svg]:w-4\",\n groupItems: \"m-0 flex list-none flex-col gap-0.5 p-0\",\n menu: \"group lg:hidden\",\n menuSummary:\n \"flex h-11 cursor-pointer list-none items-center justify-between gap-3 rounded-lg border border-border-default bg-surface-1 px-3.5 text-sm font-medium text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-details-marker]:hidden group-open:border-accent\",\n menuChevron: \"h-4 w-4 shrink-0 text-fg-secondary transition-transform group-open:rotate-180 motion-reduce:transition-none\",\n menuPanel: \"mt-2 rounded-lg border border-border-default bg-surface-1 p-2\",\n navBadge: \"ml-auto font-mono text-[11px] text-fg-secondary\",\n header: \"flex flex-wrap items-center gap-x-4 gap-y-3 border-b border-border-subtle pb-5\",\n content: \"flex min-w-0 flex-col gap-6\",\n section: \"rounded-xl border border-border-default bg-surface-1\",\n sectionHead: \"flex flex-col gap-1 px-5 pt-5\",\n sectionTitle: \"text-base font-bold text-fg\",\n sectionDescription: \"text-sm text-fg-secondary\",\n sectionBody: \"flex flex-col gap-4 px-5 py-5\",\n sectionFooter: \"flex flex-wrap items-center justify-end gap-2 border-t border-border-subtle bg-surface-2/40 px-5 py-3 rounded-b-xl\",\n // The flat look: no card, hairline-divided rows (Linear / Vercel / GitHub settings).\n rows: \"flex flex-col divide-y divide-border-subtle border-y border-border-subtle\",\n row: \"flex flex-col gap-2 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6\",\n rowText: \"flex min-w-0 flex-col gap-0.5\",\n rowLabel: \"text-sm font-medium text-fg\",\n rowDescription: \"text-sm text-fg-secondary\",\n rowControl: \"flex min-w-0 shrink-0 items-center gap-2 text-sm text-fg sm:max-w-[60%] sm:justify-end\",\n },\n variants: {\n variant: {\n card: {},\n flat: {\n content: \"max-w-[45rem] gap-10\",\n section: \"rounded-none border-0 bg-transparent\",\n sectionHead: \"gap-0.5 px-0 pb-3 pt-0\",\n sectionBody: \"gap-0 px-0 py-0\",\n sectionFooter: \"justify-start border-t-0 bg-transparent px-0 pb-0 pt-4\",\n },\n },\n },\n defaultVariants: { variant: \"card\" },\n});\n\nexport interface SettingsNavItem {\n /** Its id (the anchor of a section on one long page). */\n id: string;\n /** Its words. */\n label: string;\n /** Where it leads: a page (`/settings/billing`) or an anchor (`#billing`). */\n href: string;\n /** Its icon (a signature icon). */\n icon?: React.ReactNode;\n /** The section shown now: `aria-current`. */\n current?: boolean;\n /** A small count or status after the label (`3`, `New`). */\n badge?: React.ReactNode;\n}\n\nexport interface SettingsNavGroup {\n /** Its id. */\n id: string;\n /** The group's title (`Library`, `Money`). */\n label: string;\n items: readonly SettingsNavItem[];\n}\n\n/** Past this many sections a phone gets one menu instead of a strip of chips (« never more than about 5 items in a horizontal tab bar »). */\nexport const SETTINGS_CHIPS_MAX = 5;\n\nconst isGrouped = (nav: readonly SettingsNavItem[] | readonly SettingsNavGroup[]): nav is readonly SettingsNavGroup[] => nav.length > 0 && \"items\" in nav[0]!;\n\nexport type SettingsLayoutProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The screen's title (`Settings`). */\n title?: React.ReactNode;\n /** One sentence under it. */\n description?: React.ReactNode;\n /** What the screen is about, above the sections: who the account is, with their actions. Replaces `title` and `description`. */\n header?: React.ReactNode;\n /** The sections, alone or in titled groups. */\n nav: readonly SettingsNavItem[] | readonly SettingsNavGroup[];\n /** The navigation's accessible name (`Settings sections`). */\n navLabel: string;\n /** The app's link component for the sections (Next's `Link`); an `<a>` otherwise. */\n linkAs?: React.ElementType;\n /**\n * `card` (each form a titled card) or `flat` (one calm column of at most 45rem, rows divided by hairlines).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The current section's forms (`SettingsSection`). */\n children: React.ReactNode;\n};\n\nfunction NavLink({ item, Link, className }: { item: SettingsNavItem; Link: React.ElementType; className: string }) {\n const s = settingsLayout();\n return (\n <Link href={item.href} className={className} aria-current={item.current ? (item.href.startsWith(\"#\") ? \"location\" : \"page\") : undefined}>\n {item.icon}\n {item.label}\n {item.badge !== undefined && <span className={s.navBadge()}>{item.badge}</span>}\n </Link>\n );\n}\n\nfunction NavGroups({ groups, Link }: { groups: readonly SettingsNavGroup[]; Link: React.ElementType }) {\n const s = settingsLayout();\n return (\n <div className={s.sidebarList()}>\n {groups.map((group) => (\n <div key={group.id} className={s.group()}>\n <h2 className={s.groupLabel()}>{group.label}</h2>\n <ul className={s.groupItems()}>\n {group.items.map((item) => (\n <li key={item.id}>\n <NavLink item={item} Link={Link} className={s.groupLink()} />\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n );\n}\n\n/** A settings screen: sections on the left (chips on a phone, one menu past five), forms on the right. */\nexport function SettingsLayout({ title, description, header, nav, navLabel, linkAs: Link = \"a\", variant = \"card\", className, children, ...props }: SettingsLayoutProps) {\n const s = settingsLayout({ variant });\n const groups: readonly SettingsNavGroup[] = isGrouped(nav) ? nav : [{ id: \"sections\", label: \"\", items: nav }];\n const items = groups.flatMap((g) => g.items);\n const asMenu = items.length > SETTINGS_CHIPS_MAX;\n const current = items.find((i) => i.current);\n return (\n <div className={s.root({ className })} {...props}>\n {header ? (\n <div className={s.header()}>{header}</div>\n ) : (\n (title || description) && (\n <div className={s.head()}>\n {title && <h1 className={s.title()}>{title}</h1>}\n {description && <p className={s.description()}>{description}</p>}\n </div>\n )\n )}\n <div className={s.columns()}>\n {asMenu ? (\n <nav aria-label={navLabel}>\n <details className={s.menu()} data-settings-menu=\"\">\n <summary className={s.menuSummary()}>\n <span className=\"truncate\">{current?.label ?? navLabel}</span>\n <svg aria-hidden viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={s.menuChevron()}>\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </summary>\n <div className={s.menuPanel()}>\n <NavGroups groups={groups} Link={Link} />\n </div>\n </details>\n <div className={s.sidebar()}>\n <NavGroups groups={groups} Link={Link} />\n </div>\n </nav>\n ) : (\n <nav aria-label={navLabel} className={s.nav()}>\n <ul className={s.navList()}>\n {items.map((item) => (\n <li key={item.id}>\n <NavLink item={item} Link={Link} className={s.navLink()} />\n </li>\n ))}\n </ul>\n </nav>\n )}\n <div className={s.content()}>{children}</div>\n </div>\n </div>\n );\n}\n\nexport interface SectionSaveLabels {\n /** “Save”. */\n save: string;\n /** “Saving…”. */\n saving: string;\n /** “Saved”. */\n saved: string;\n /** What failed, one sentence (“Could not save. Try again.”). */\n error: string;\n /** “Undo” — only shown after a save, when `onUndo` is given. */\n undo: string;\n /** “Discard changes” — only shown while dirty, when `onDiscard` is given. */\n discard: string;\n}\n\n/** The one save of a section: its dirty state, its status, its words. */\nexport interface SectionSave {\n /** Whether a field differs from what is saved: the save is enabled only then. */\n dirty: boolean;\n /** Where the save is (`useSaveStatus().status`). */\n status: SaveStatus;\n /** Saves the section (the form's submit, Enter included). */\n onSave: () => void;\n /** Puts the previous values back after a save, optimistic update included. */\n onUndo?: () => void;\n /** Throws the edits away while dirty. */\n onDiscard?: () => void;\n labels: SectionSaveLabels;\n}\n\nexport type SettingsSectionProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n /**\n * One save for the whole section: the section becomes a form, the foot shows the Save (enabled while dirty), the\n * result in a live region (saved, or the error as an alert) and an undo after a save. Replaces `footer`.\n */\n save?: SectionSave;\n /** The section's title: it names the region (and is its anchor's target when `id` is set). */\n title: React.ReactNode;\n /** What these settings change, in one sentence. */\n description?: React.ReactNode;\n /** At the foot: the section's own save (a Button), a note. */\n footer?: React.ReactNode;\n /**\n * `card` (a titled card) or `flat` (no card: a title, then hairline-divided `SettingsRow`s).\n * @default \"card\"\n */\n variant?: \"card\" | \"flat\";\n /** The fields (or, flat, the `SettingsRow`s). A section of a title, a sentence and a foot action has none. */\n children?: React.ReactNode;\n};\n\n/** A group of settings: a titled card with its fields and its own save, or flat rows under a title. */\nexport function SettingsSection({ title, description, footer, save, variant = \"card\", id, className, children, ...props }: SettingsSectionProps) {\n const s = settingsLayout({ variant });\n const auto = React.useId();\n const titleId = `${id ?? auto}-title`;\n const head = (\n <div className={s.sectionHead()}>\n <h2 id={titleId} className={s.sectionTitle()}>\n {title}\n </h2>\n {description && <p className={s.sectionDescription()}>{description}</p>}\n </div>\n );\n const body = children === undefined ? null : (\n <div className={variant === \"flat\" ? s.rows() : s.sectionBody()} data-settings-rows={variant === \"flat\" ? \"\" : undefined}>\n {children}\n </div>\n );\n return (\n <section id={id} aria-labelledby={titleId} data-dirty={save?.dirty ? \"\" : undefined} className={s.section({ className })} {...props}>\n {save ? (\n <form\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n if (save.dirty && save.status !== SaveStatus.Saving) save.onSave();\n }}\n >\n {head}\n {body}\n <div className={s.sectionFooter()}>\n <SaveFeedback save={save} />\n {save.onDiscard && save.dirty ? (\n <Button type=\"button\" variant=\"ghost\" onClick={save.onDiscard}>\n {save.labels.discard}\n </Button>\n ) : null}\n <Button type=\"submit\" variant=\"primary\" disabled={!save.dirty || save.status === SaveStatus.Saving} loading={save.status === SaveStatus.Saving}>\n {save.labels.save}\n </Button>\n </div>\n </form>\n ) : (\n <>\n {head}\n {body}\n {footer && <div className={s.sectionFooter()}>{footer}</div>}\n </>\n )}\n </section>\n );\n}\n\n/** The result of the last save: « Saved » (with Undo) in a polite live region, a failure as an alert. The region stays mounted so the change is announced. */\nfunction SaveFeedback({ save }: { save: SectionSave }) {\n const failed = save.status === SaveStatus.Error;\n const saved = save.status === SaveStatus.Saved;\n return (\n <div className=\"mr-auto flex min-w-0 items-center gap-2 text-sm\">\n <span role={failed ? \"alert\" : \"status\"} aria-live={failed ? \"assertive\" : \"polite\"} className={failed ? \"text-danger-text\" : \"text-fg-secondary\"}>\n {failed ? save.labels.error : saved ? save.labels.saved : save.status === SaveStatus.Saving ? save.labels.saving : \"\"}\n </span>\n {saved && save.onUndo ? (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={save.onUndo}>\n {save.labels.undo}\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport type SettingsRowProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** What the setting is (`Username`). */\n label: React.ReactNode;\n /** The help text under it, one sentence. */\n description?: React.ReactNode;\n /** The id of the control, so the label names it (a `<label htmlFor>`). Leave out for a plain value. */\n htmlFor?: string;\n /** The control or the value, on the right (stacked under the label on a phone). */\n children?: React.ReactNode;\n};\n\n/** One setting: its label and help on the left, its control or value on the right. Put rows in a flat `SettingsSection`. */\nexport function SettingsRow({ label, description, htmlFor, className, children, ...props }: SettingsRowProps) {\n const s = settingsLayout();\n const Label = htmlFor ? \"label\" : \"span\";\n return (\n <div className={s.row({ className })} {...props}>\n <div className={s.rowText()}>\n <Label className={s.rowLabel()} {...(htmlFor ? { htmlFor } : {})}>\n {label}\n </Label>\n {description && <span className={s.rowDescription()}>{description}</span>}\n </div>\n {children !== undefined && <div className={s.rowControl()}>{children}</div>}\n </div>\n );\n}\n"
340
364
  }
341
365
  ],
342
366
  "dependencies": [
@@ -355,15 +379,20 @@
355
379
  "description": "How long « Saved » stays before the section goes quiet again.",
356
380
  "props": []
357
381
  },
382
+ {
383
+ "component": "SETTINGS_CHIPS_MAX",
384
+ "description": "Past this many sections a phone gets one menu instead of a strip of chips (« never more than about 5 items in a horizontal tab bar »).",
385
+ "props": []
386
+ },
358
387
  {
359
388
  "component": "SettingsLayout",
360
- "description": "A settings screen: sections on the left (chips on a phone), forms on the right.",
389
+ "description": "A settings screen: sections on the left (chips on a phone, one menu past five), forms on the right.",
361
390
  "props": [
362
391
  {
363
392
  "name": "nav",
364
- "type": "readonly SettingsNavItem[]",
393
+ "type": "readonly SettingsNavItem[] | readonly SettingsNavGroup[]",
365
394
  "default": null,
366
- "description": "The sections.",
395
+ "description": "The sections, alone or in titled groups.",
367
396
  "required": true
368
397
  },
369
398
  {
@@ -380,6 +409,13 @@
380
409
  "description": "One sentence under it.",
381
410
  "required": false
382
411
  },
412
+ {
413
+ "name": "header",
414
+ "type": "ReactNode",
415
+ "default": null,
416
+ "description": "What the screen is about, above the sections: who the account is, with their actions. Replaces `title` and `description`.",
417
+ "required": false
418
+ },
383
419
  {
384
420
  "name": "linkAs",
385
421
  "type": "ElementType<any, keyof IntrinsicElements>",
@@ -343,7 +343,7 @@
343
343
  }
344
344
  ],
345
345
  "dependencies": [
346
- "@krizaka/tokens@^3.48.0",
346
+ "@krizaka/tokens@^3.50.0",
347
347
  "radix-ui@^1.7.0",
348
348
  "tailwind-variants@^3.3.1"
349
349
  ],