@sableui/react 0.5.0 → 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/AGENTS.md +2 -2
- package/CHANGELOG.md +135 -0
- package/CONTRIBUTING.md +1 -1
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/bin/sable-lint.mjs +2 -1
- package/bin/sable.mjs +3 -2
- package/dist/{_contract-Cn3lUpih.d.ts → _contract-C2M1SjBf.d.ts} +20 -2
- package/dist/blocks/index.js +19 -51
- package/dist/{chunk-RCLKXF2N.js → chunk-7QSDYBXO.js} +610 -30
- package/dist/{chunk-GU3T7SBA.js → chunk-EDXLM7YL.js} +1 -1
- package/dist/chunk-FKZXAFIY.js +12 -0
- package/dist/{chunk-OQACYT2D.js → chunk-NETUFI34.js} +23 -1
- package/dist/{chunk-ZBZMKOO7.js → chunk-VVW4UCDI.js} +3 -3
- package/dist/foundations/index.js +6 -0
- package/dist/index.d.ts +127 -11
- package/dist/index.js +23 -13
- package/dist/ios/index.d.ts +6 -1
- package/dist/ios/index.js +4 -3
- package/dist/ios/styles.css +9 -0
- package/dist/patterns/index.js +22 -28
- package/dist/presets/index.d.ts +108 -5
- package/dist/presets/index.js +239 -6
- package/dist/styles.css +50 -0
- package/dist/{theme-provider-CGeiusgu.d.ts → theme-provider-kQszHYji.d.ts} +1 -1
- package/dist/themes/atelier/atelier.css +31 -3
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +38 -7
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +33 -4
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +28 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +26 -0
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +24 -11
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +34 -4
- package/llms/blocks/danger-zone-block.md +1 -1
- package/llms/blocks/event-card-block.md +1 -1
- package/llms/blocks/feedback-form-block.md +1 -1
- package/llms/blocks/invite-team-block.md +1 -1
- package/llms/blocks/media-player-block.md +3 -16
- package/llms/blocks/notification-list-block.md +3 -3
- package/llms/blocks/product-card-block.md +3 -17
- package/llms/blocks/profile-card-block.md +4 -17
- package/llms/blocks/profile-form-block.md +2 -2
- package/llms/blocks/team-members-block.md +1 -1
- package/llms/blocks/testimonial-block.md +1 -1
- package/llms/components/avatar.md +13 -1
- package/llms/components/card.md +1 -1
- package/llms/components/gradient-art.md +80 -0
- package/llms/components/sheet.md +1 -1
- package/llms/foundations/accessibility.md +1 -0
- package/llms/patterns/auth-page.md +5 -19
- package/llms/patterns/brand-landing-page.md +1 -1
- package/llms/patterns/chat-panel.md +1 -1
- package/llms/patterns/dashboard-page.md +4 -4
- package/llms/patterns/feed-page.md +4 -4
- package/llms/patterns/landing-page.md +1 -1
- package/llms/patterns/list-detail-page.md +3 -2
- package/llms/patterns/settings-page.md +3 -3
- package/llms/reference/token-intent.md +9 -1
- package/llms/tasks/auditing-an-app.md +6 -0
- package/llms/tasks/building-an-ios-prototype.md +3 -1
- package/llms/tasks/converting-an-app.md +1 -1
- package/llms/tasks/creating-a-preset.md +24 -0
- package/llms/tasks/reviewing-a-design.md +9 -0
- package/llms/tasks/using-presets.md +6 -1
- package/llms-full.txt +201 -104
- package/llms.txt +3 -2
- package/package.json +9 -7
- package/registry.json +279 -30
- package/scripts/design-presets.mjs +27 -4
- package/scripts/grade.mjs +2 -1
- package/scripts/label-contrast.mjs +138 -0
- package/scripts/lint-rules.mjs +23 -2
- package/skills/sable/SKILL.md +3 -1
- package/targets/ios.json +18 -2
- package/third-party/paper-shaders-LICENSE.txt +201 -0
- package/third-party/paper-shaders-NOTICE.txt +5 -0
package/dist/patterns/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { TextReveal, Reveal, MessageScroller, Marker, Toolbar, ToolbarButton, StreamingText, ChatComposer, SettingsMenu, SettingsRow, AppShell, PageHeader, DataTable, Meter, RelativeTime, Sidebar, SidebarHeader, SidebarNav, SidebarItem, NotificationBadge, SidebarFooter, BottomNav, BottomNavItem, AppHeader, Tabs, TabsList, TabsTrigger, TabsPanel, Skeleton, Browser, MeterLabel, MeterValue, MeterTrack, SearchInput, EmptyState, SideNav, SideNavItem } from '../chunk-
|
|
2
|
-
import { Form, Field, Input, Button, Separator, Card, CardContent, Badge, Entity, Avatar, StatusDot, CardHeader, CardTitle, CardDescription, CardAction, Message, Bubble, CodeBlock, SimpleTooltip, Attachment, CardFooter, MenuRadioGroup, MenuRadioItem, CardList, OverflowMenu, MenuItem, MenuSeparator, Textarea, Code, Select, SelectTrigger, SelectContent, SelectItem, DestructiveActionModal, Switch } from '../chunk-
|
|
1
|
+
import { TextReveal, Reveal, MessageScroller, Marker, Toolbar, ToolbarButton, StreamingText, ChatComposer, SettingsMenu, SettingsRow, AppShell, PageHeader, DataTable, Meter, RelativeTime, Sidebar, SidebarHeader, SidebarNav, SidebarItem, NotificationBadge, SidebarFooter, BottomNav, BottomNavItem, AppHeader, Tabs, TabsList, TabsTrigger, TabsPanel, Skeleton, Browser, MeterLabel, MeterValue, MeterTrack, SearchInput, EmptyState, SideNav, SideNavItem } from '../chunk-VVW4UCDI.js';
|
|
2
|
+
import { Form, Field, Input, Button, Separator, GradientArt, Card, CardContent, Badge, Entity, Avatar, StatusDot, CardHeader, CardTitle, CardDescription, CardAction, Message, Bubble, CodeBlock, SimpleTooltip, Attachment, CardFooter, MenuRadioGroup, MenuRadioItem, CardList, OverflowMenu, MenuItem, MenuSeparator, Textarea, Code, Select, SelectTrigger, SelectContent, SelectItem, DestructiveActionModal, Switch } from '../chunk-7QSDYBXO.js';
|
|
3
|
+
import '../chunk-FKZXAFIY.js';
|
|
3
4
|
import { Region, Stack, Row, Grid, Page, Columns, Col, AspectBox } from '../chunk-3IWZYBGH.js';
|
|
4
5
|
import '../chunk-SRQWQUTW.js';
|
|
5
6
|
import { Icon } from '../chunk-QJ6FWTUG.js';
|
|
@@ -7,17 +8,6 @@ import '../chunk-XAZGMY73.js';
|
|
|
7
8
|
import { useState, useRef, useEffect } from 'react';
|
|
8
9
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
10
|
|
|
10
|
-
var artwork = [
|
|
11
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
12
|
-
"radial-gradient(in oklab at 18% 22%, oklch(from var(--tone-info) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 58%)",
|
|
13
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
14
|
-
"radial-gradient(in oklab at 82% 18%, oklch(from var(--tone-collectible) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)",
|
|
15
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
16
|
-
"radial-gradient(in oklab at 70% 88%, oklch(from var(--tone-edition) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 60%)",
|
|
17
|
-
// design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
|
|
18
|
-
"radial-gradient(in oklab at 12% 92%, oklch(from var(--tone-standard) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h), transparent 55%)",
|
|
19
|
-
"var(--primary)"
|
|
20
|
-
].join(", ");
|
|
21
11
|
function AuthPage() {
|
|
22
12
|
const [pending2, setPending] = useState(false);
|
|
23
13
|
const [errors, setErrors] = useState({});
|
|
@@ -76,14 +66,14 @@ function AuthPage() {
|
|
|
76
66
|
"."
|
|
77
67
|
] })
|
|
78
68
|
] }) }),
|
|
79
|
-
/* @__PURE__ */ jsx("aside", { "aria-label": "Featured release", className: "hidden p-3 lg:flex", children: /* @__PURE__ */ jsx("
|
|
69
|
+
/* @__PURE__ */ jsx("aside", { "aria-label": "Featured release", className: "hidden p-3 lg:flex", children: /* @__PURE__ */ jsx(GradientArt, { seed: "Untitled (gold rush)", className: "flex flex-1 flex-col justify-end rounded-xl p-8", children: /* @__PURE__ */ jsx(Card, { className: "max-w-md", children: /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
80
70
|
/* @__PURE__ */ jsx(Badge, { tone: "edition", className: "self-start", children: "Edition of 50" }),
|
|
81
71
|
/* @__PURE__ */ jsx("p", { className: "text-title-lg", children: "\u201CEvery edition is a conversation with whoever holds it next.\u201D" }),
|
|
82
72
|
/* @__PURE__ */ jsx(
|
|
83
73
|
Entity,
|
|
84
74
|
{
|
|
85
75
|
className: "p-0",
|
|
86
|
-
leading: /* @__PURE__ */ jsx(Avatar, { fallback: "MO" }),
|
|
76
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: "MO" }),
|
|
87
77
|
title: "Mara Okafor",
|
|
88
78
|
subtitle: "Untitled (gold rush), 2026"
|
|
89
79
|
}
|
|
@@ -353,7 +343,7 @@ function BrandLandingPage() {
|
|
|
353
343
|
/* @__PURE__ */ jsx("blockquote", { className: "text-balance text-heading-3 sm:text-heading-2", children: "\u201CWe used to chase approvals across four tools. Now every request has an owner, a deadline, and a record, and our auditors stopped asking for screenshots.\u201D" })
|
|
354
344
|
] }),
|
|
355
345
|
/* @__PURE__ */ jsxs("figcaption", { className: "flex items-center gap-3", children: [
|
|
356
|
-
/* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: "MO" }),
|
|
346
|
+
/* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: "MO" }),
|
|
357
347
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
358
348
|
/* @__PURE__ */ jsx("p", { className: "text-title-sm", children: "Maya Okafor" }),
|
|
359
349
|
/* @__PURE__ */ jsx("p", { className: "text-body-sm text-muted-foreground", children: "Head of operations, Fielddesk" })
|
|
@@ -486,6 +476,7 @@ function ChatPanel() {
|
|
|
486
476
|
const assistant = /* @__PURE__ */ jsx(
|
|
487
477
|
Avatar,
|
|
488
478
|
{
|
|
479
|
+
variant: "art",
|
|
489
480
|
size: "sm",
|
|
490
481
|
alt: "Assistant",
|
|
491
482
|
fallback: /* @__PURE__ */ jsx(Icon, { name: "sparkles", className: "text-base" })
|
|
@@ -711,7 +702,7 @@ function DashboardPage() {
|
|
|
711
702
|
/* @__PURE__ */ jsx(Icon, { name: "gem", variant: "solid", className: "text-xl" }),
|
|
712
703
|
/* @__PURE__ */ jsx("span", { className: "text-title-sm", children: "Atelier" })
|
|
713
704
|
] }),
|
|
714
|
-
actions: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: "AL", alt: "Ada Lovelace" })
|
|
705
|
+
actions: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: "AL", alt: "Ada Lovelace" })
|
|
715
706
|
}
|
|
716
707
|
),
|
|
717
708
|
bottomNav: /* @__PURE__ */ jsxs(BottomNav, { "aria-label": "Primary", children: [
|
|
@@ -745,7 +736,7 @@ function DashboardPage() {
|
|
|
745
736
|
Entity,
|
|
746
737
|
{
|
|
747
738
|
className: "[--entity-py:0]",
|
|
748
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: "AL", alt: "" }),
|
|
739
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: "AL", alt: "" }),
|
|
749
740
|
title: "Ada Lovelace",
|
|
750
741
|
subtitle: "@ada",
|
|
751
742
|
action: /* @__PURE__ */ jsxs(OverflowMenu, { label: "Account", size: "icon-sm", children: [
|
|
@@ -890,7 +881,7 @@ function DashboardPage() {
|
|
|
890
881
|
/* @__PURE__ */ jsx(CardList, { children: collectors.map((person) => /* @__PURE__ */ jsx(
|
|
891
882
|
Entity,
|
|
892
883
|
{
|
|
893
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: person.initials, alt: "" }),
|
|
884
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: person.initials, alt: "" }),
|
|
894
885
|
title: person.name,
|
|
895
886
|
subtitle: `${person.handle} \xB7 ${person.pieces} pieces`,
|
|
896
887
|
action: /* @__PURE__ */ jsx("span", { className: "text-mono-sm", children: usd.format(person.spent) })
|
|
@@ -929,7 +920,7 @@ function DashboardPage() {
|
|
|
929
920
|
/* @__PURE__ */ jsx(CardList, { children: activity.map((item) => /* @__PURE__ */ jsx(
|
|
930
921
|
Entity,
|
|
931
922
|
{
|
|
932
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: item.initials, alt: "" }),
|
|
923
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: item.initials, alt: "" }),
|
|
933
924
|
title: item.who,
|
|
934
925
|
subtitle: item.what,
|
|
935
926
|
action: /* @__PURE__ */ jsx(RelativeTime, { date: Date.now() - item.when, className: "text-caption text-muted-foreground" })
|
|
@@ -1019,7 +1010,7 @@ var renderPost = (post) => /* @__PURE__ */ jsx("article", { children: /* @__PURE
|
|
|
1019
1010
|
/* @__PURE__ */ jsx(CardList, { children: /* @__PURE__ */ jsx(
|
|
1020
1011
|
Entity,
|
|
1021
1012
|
{
|
|
1022
|
-
leading: /* @__PURE__ */ jsx(Avatar, { fallback: post.initials, alt: post.author }),
|
|
1013
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: post.initials, alt: post.author }),
|
|
1023
1014
|
title: post.author,
|
|
1024
1015
|
subtitle: `${post.handle} \xB7 ${post.time}`,
|
|
1025
1016
|
action: /* @__PURE__ */ jsxs(OverflowMenu, { label: `Post options for ${post.author}`, size: "icon-sm", children: [
|
|
@@ -1082,7 +1073,7 @@ function FeedPage() {
|
|
|
1082
1073
|
/* @__PURE__ */ jsxs(Stack, { gap: 3, className: "min-w-0 max-w-(--region-feed) flex-1", children: [
|
|
1083
1074
|
/* @__PURE__ */ jsx(PageHeader, { title: "Home" }),
|
|
1084
1075
|
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(Row, { gap: 1.5, align: "start", children: [
|
|
1085
|
-
/* @__PURE__ */ jsx(Avatar, { fallback: "AL", alt: "Ada Lovelace" }),
|
|
1076
|
+
/* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: "AL", alt: "Ada Lovelace" }),
|
|
1086
1077
|
/* @__PURE__ */ jsxs(Stack, { gap: 1.5, className: "min-w-0 flex-1", children: [
|
|
1087
1078
|
/* @__PURE__ */ jsx(
|
|
1088
1079
|
Textarea,
|
|
@@ -1131,7 +1122,7 @@ function FeedPage() {
|
|
|
1131
1122
|
/* @__PURE__ */ jsx(CardList, { children: suggestions.map((person) => /* @__PURE__ */ jsx(
|
|
1132
1123
|
Entity,
|
|
1133
1124
|
{
|
|
1134
|
-
leading: /* @__PURE__ */ jsx(Avatar, { fallback: person.initials, alt: person.name }),
|
|
1125
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: person.initials, alt: person.name }),
|
|
1135
1126
|
title: person.name,
|
|
1136
1127
|
subtitle: person.handle,
|
|
1137
1128
|
action: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", "aria-label": `Follow ${person.name}`, children: "Follow" })
|
|
@@ -1144,7 +1135,7 @@ function FeedPage() {
|
|
|
1144
1135
|
/* @__PURE__ */ jsx(CardList, { children: upcoming2.map((drop) => /* @__PURE__ */ jsx(
|
|
1145
1136
|
Entity,
|
|
1146
1137
|
{
|
|
1147
|
-
leading: /* @__PURE__ */ jsx(Avatar, { fallback: drop.initials, alt: drop.artist }),
|
|
1138
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", fallback: drop.initials, alt: drop.artist }),
|
|
1148
1139
|
title: drop.title,
|
|
1149
1140
|
subtitle: `${drop.artist} \xB7 ${drop.date}`,
|
|
1150
1141
|
action: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": `Remind me about ${drop.title}`, children: /* @__PURE__ */ jsx(Icon, { name: "bell" }) })
|
|
@@ -1394,7 +1385,7 @@ function LandingPage() {
|
|
|
1394
1385
|
/* @__PURE__ */ jsx(CardList, { children: deploys.map((deploy) => /* @__PURE__ */ jsx(
|
|
1395
1386
|
Entity,
|
|
1396
1387
|
{
|
|
1397
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: deploy.initials, alt: deploy.name }),
|
|
1388
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: deploy.initials, alt: deploy.name }),
|
|
1398
1389
|
title: deploy.title,
|
|
1399
1390
|
subtitle: deploy.meta,
|
|
1400
1391
|
action: /* @__PURE__ */ jsx(Badge, { appearance: "plain", tone: deploy.tone, dot: true, pulse: deploy.pulse, children: deploy.status })
|
|
@@ -1721,6 +1712,8 @@ function ListDetailPage() {
|
|
|
1721
1712
|
leading: /* @__PURE__ */ jsx(
|
|
1722
1713
|
Avatar,
|
|
1723
1714
|
{
|
|
1715
|
+
variant: "art",
|
|
1716
|
+
seed: m.name,
|
|
1724
1717
|
fallback: m.pending ? /* @__PURE__ */ jsx(Icon, { name: "mail", className: "size-5" }) : m.initials,
|
|
1725
1718
|
status: m.lastActive === "Online now" ? /* @__PURE__ */ jsx(StatusDot, { tone: "success" }) : void 0,
|
|
1726
1719
|
"aria-hidden": true
|
|
@@ -1773,6 +1766,7 @@ function ListDetailPage() {
|
|
|
1773
1766
|
/* @__PURE__ */ jsx(
|
|
1774
1767
|
Avatar,
|
|
1775
1768
|
{
|
|
1769
|
+
variant: "art",
|
|
1776
1770
|
size: "lg",
|
|
1777
1771
|
fallback: selected.pending ? /* @__PURE__ */ jsx(Icon, { name: "mail", className: "size-6" }) : selected.initials,
|
|
1778
1772
|
status: selected.lastActive === "Online now" ? /* @__PURE__ */ jsx(StatusDot, { tone: "success" }) : void 0,
|
|
@@ -1928,7 +1922,7 @@ function SettingsPage() {
|
|
|
1928
1922
|
/* @__PURE__ */ jsx(Icon, { name: "gem", variant: "solid", className: "text-xl" }),
|
|
1929
1923
|
/* @__PURE__ */ jsx("span", { className: "text-title-sm", children: "Atelier" })
|
|
1930
1924
|
] }),
|
|
1931
|
-
actions: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: "AL", alt: "Ada Lovelace" })
|
|
1925
|
+
actions: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: "AL", alt: "Ada Lovelace" })
|
|
1932
1926
|
}
|
|
1933
1927
|
),
|
|
1934
1928
|
bottomNav: /* @__PURE__ */ jsxs(BottomNav, { "aria-label": "Primary", children: [
|
|
@@ -1962,7 +1956,7 @@ function SettingsPage() {
|
|
|
1962
1956
|
Entity,
|
|
1963
1957
|
{
|
|
1964
1958
|
className: "[--entity-py:0]",
|
|
1965
|
-
leading: /* @__PURE__ */ jsx(Avatar, { size: "sm", fallback: "AL", alt: "" }),
|
|
1959
|
+
leading: /* @__PURE__ */ jsx(Avatar, { variant: "art", size: "sm", fallback: "AL", alt: "" }),
|
|
1966
1960
|
title: "Ada Lovelace",
|
|
1967
1961
|
subtitle: "@ada",
|
|
1968
1962
|
action: /* @__PURE__ */ jsxs(OverflowMenu, { label: "Account", size: "icon-sm", children: [
|
|
@@ -2003,7 +1997,7 @@ function SettingsPage() {
|
|
|
2003
1997
|
/* @__PURE__ */ jsxs(Form, { onSubmit: (event) => event.preventDefault(), className: "gap-0", children: [
|
|
2004
1998
|
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
2005
1999
|
/* @__PURE__ */ jsxs(Row, { gap: 2, children: [
|
|
2006
|
-
/* @__PURE__ */ jsx(Avatar, { size: "lg", fallback: "AL", alt: "Ada Lovelace" }),
|
|
2000
|
+
/* @__PURE__ */ jsx(Avatar, { variant: "art", size: "lg", fallback: "AL", alt: "Ada Lovelace" }),
|
|
2007
2001
|
/* @__PURE__ */ jsxs(Stack, { gap: 0.5, align: "start", children: [
|
|
2008
2002
|
/* @__PURE__ */ jsxs(Row, { gap: 1, children: [
|
|
2009
2003
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", children: "Upload photo" }),
|
package/dist/presets/index.d.ts
CHANGED
|
@@ -1,4 +1,81 @@
|
|
|
1
|
-
import { T as ThemeManifest } from '../_contract-
|
|
1
|
+
import { T as ThemeManifest } from '../_contract-C2M1SjBf.js';
|
|
2
|
+
|
|
3
|
+
/** An OKLCH colour, lightness 0–1. Extra fields (alpha) ride along untouched. */
|
|
4
|
+
interface OklchColor {
|
|
5
|
+
l: number;
|
|
6
|
+
c: number;
|
|
7
|
+
h: number;
|
|
8
|
+
}
|
|
9
|
+
/** Ratio and Lc of a label on a fill, and whether it clears both floors. */
|
|
10
|
+
declare function labelContrast(ink: OklchColor, fill: OklchColor): {
|
|
11
|
+
ratio: number;
|
|
12
|
+
lc: number;
|
|
13
|
+
reads: boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The ink for a fill that cannot move (a status colour, a decorative tone).
|
|
17
|
+
* One that reads; of two, the stronger. Of none, the one meeting the ratio,
|
|
18
|
+
* because APCA cannot waive it; failing that, the stronger.
|
|
19
|
+
*/
|
|
20
|
+
declare function inkFor<T extends OklchColor>(fill: OklchColor, inks: readonly T[]): T;
|
|
21
|
+
/**
|
|
22
|
+
* The fill nearest `fill` on which `ink` reads: lightness moves away from the
|
|
23
|
+
* ink in small steps, hue holds, and chroma is kept unless the new lightness
|
|
24
|
+
* cannot show it. Returns `fill` itself when the ink already reads, and the
|
|
25
|
+
* furthest step tried when nothing in range does.
|
|
26
|
+
*/
|
|
27
|
+
declare function fitFillToInk<T extends OklchColor>(fill: T, ink: OklchColor): T;
|
|
28
|
+
/**
|
|
29
|
+
* Which ink a colour takes: the one that reads on it with the least change to
|
|
30
|
+
* it — none, when one already reads (the stronger, when both do). Decide this
|
|
31
|
+
* once, on the colour as picked, then `fitFillToInk` each mode's fill to it.
|
|
32
|
+
*/
|
|
33
|
+
declare function inkPolarity<T extends OklchColor>(color: OklchColor, inks: readonly T[]): T;
|
|
34
|
+
/** Fills a theme or preset authors from its brand. Their ink follows the colour as picked. */
|
|
35
|
+
declare const BRAND_FILLS: readonly ["primary", "sidebar-primary", "highlight"];
|
|
36
|
+
/**
|
|
37
|
+
* Status fills. Each has a separate `-text` token for the colour drawn on a
|
|
38
|
+
* surface (`text-destructive` reads `--destructive-text`), so the fill is free to
|
|
39
|
+
* sit where its label reads — a dark-mode red under white ink — without taking
|
|
40
|
+
* the page's error text down with it.
|
|
41
|
+
*/
|
|
42
|
+
declare const STATUS_FILLS: readonly ["destructive", "success", "tone-warning", "tone-info"];
|
|
43
|
+
/**
|
|
44
|
+
* Every fill a label sits on that the rule measures. Sable's own themes are held
|
|
45
|
+
* to it (themes.test.ts); a preset or an app is told, offered a fix, and may keep
|
|
46
|
+
* its choice on the record.
|
|
47
|
+
*/
|
|
48
|
+
declare const LABELLED_FILLS: readonly ["primary", "sidebar-primary", "highlight", "destructive", "success", "tone-warning", "tone-info"];
|
|
49
|
+
/** The rule's id, shared by `sable-lint`, `sable preset --check` and preset `exceptions`. */
|
|
50
|
+
declare const LABEL_CONTRAST_RULE = "label-contrast";
|
|
51
|
+
interface LabelFinding {
|
|
52
|
+
mode: "light" | "dark";
|
|
53
|
+
/** The fill token, without `--`; its ink is `${fill}-foreground`. */
|
|
54
|
+
fill: string;
|
|
55
|
+
ratio: number;
|
|
56
|
+
lc: number;
|
|
57
|
+
/** The nearest pair that reads, decided as the customizer decides it. */
|
|
58
|
+
suggestion: {
|
|
59
|
+
fill: string;
|
|
60
|
+
ink: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check each labelled fill against its ink, in both modes.
|
|
65
|
+
*
|
|
66
|
+
* Takes resolved values (`oklch(…)` or hex; anything else is counted as
|
|
67
|
+
* unmeasured, never guessed). Which ink a fix uses is decided on the LIGHT-mode
|
|
68
|
+
* fill — the colour as picked — so a dark-mode failure on a colour that takes
|
|
69
|
+
* white is fixed by bringing the fill down, not by flipping the label dark.
|
|
70
|
+
*/
|
|
71
|
+
declare function checkLabelledFills(tokens: {
|
|
72
|
+
light: Record<string, string | undefined>;
|
|
73
|
+
dark: Record<string, string | undefined>;
|
|
74
|
+
}): {
|
|
75
|
+
findings: LabelFinding[];
|
|
76
|
+
measured: number;
|
|
77
|
+
unmeasured: string[];
|
|
78
|
+
};
|
|
2
79
|
|
|
3
80
|
/** The public theme vocabulary. Presets never need component overrides. */
|
|
4
81
|
declare const PRESET_TOKEN_NAMES: readonly string[];
|
|
@@ -26,12 +103,38 @@ interface SablePreset {
|
|
|
26
103
|
light: Record<string, string>;
|
|
27
104
|
dark: Record<string, string>;
|
|
28
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Sable's opinions the preset declines, on the record. A check reports an
|
|
108
|
+
* accepted finding with its reason instead of failing on it.
|
|
109
|
+
*/
|
|
110
|
+
exceptions?: PresetException[];
|
|
111
|
+
}
|
|
112
|
+
/** A deliberate departure from a Sable check — e.g. a brand that requires dark ink on its primary. */
|
|
113
|
+
interface PresetException {
|
|
114
|
+
rule: typeof LABEL_CONTRAST_RULE;
|
|
115
|
+
mode: "light" | "dark";
|
|
116
|
+
/** The fill token, e.g. `primary`. */
|
|
117
|
+
token: (typeof LABELLED_FILLS)[number];
|
|
118
|
+
reason: string;
|
|
29
119
|
}
|
|
30
|
-
|
|
31
|
-
declare function
|
|
32
|
-
declare function parsePreset(input: unknown): SablePreset;
|
|
120
|
+
declare function validatePreset(raw: unknown): string[];
|
|
121
|
+
declare function parsePreset(raw: unknown): SablePreset;
|
|
33
122
|
declare function presetManifest(input: SablePreset): ThemeManifest;
|
|
34
123
|
/** Scoped CSS can coexist with built-in presets. Never changes :root globally. */
|
|
35
124
|
declare function presetCss(input: SablePreset): string;
|
|
125
|
+
/**
|
|
126
|
+
* Labels on the preset's brand and status fills (contrast.ts, "Labels on fills"): each
|
|
127
|
+
* failing pair with the nearest pair that reads. Findings the preset accepts in
|
|
128
|
+
* `exceptions` come back under `accepted` with their reason — reported, not
|
|
129
|
+
* failed, because the choice is the preset author's to make.
|
|
130
|
+
*/
|
|
131
|
+
declare function presetLabelContrast(input: SablePreset): {
|
|
132
|
+
open: LabelFinding[];
|
|
133
|
+
accepted: (LabelFinding & {
|
|
134
|
+
reason: string;
|
|
135
|
+
})[];
|
|
136
|
+
measured: number;
|
|
137
|
+
unmeasured: string[];
|
|
138
|
+
};
|
|
36
139
|
|
|
37
|
-
export { PRESET_TOKEN_NAMES, type SablePreset, parsePreset, presetCss, presetManifest, validatePreset };
|
|
140
|
+
export { BRAND_FILLS, LABELLED_FILLS, LABEL_CONTRAST_RULE, type LabelFinding, type OklchColor, PRESET_TOKEN_NAMES, type PresetException, STATUS_FILLS, type SablePreset, checkLabelledFills, fitFillToInk, inkFor, inkPolarity, labelContrast, parsePreset, presetCss, presetLabelContrast, presetManifest, validatePreset };
|
package/dist/presets/index.js
CHANGED
|
@@ -1,10 +1,193 @@
|
|
|
1
|
-
import { SEMANTIC_TOKENS, WORKSPACE_SURFACE_TOKENS, DERIVED_EDGE_TOKENS, RADIUS_TOKENS, MOTION_TOKENS, TYPE_SCALE_TOKENS, TYPE_REFINEMENT_TOKENS, TYPE_OVERLINE_TOKENS, DENSITY_TOKENS, LAYERING_TOKENS, TYPE_UI_TOKENS } from '../chunk-
|
|
1
|
+
import { SEMANTIC_TOKENS, WORKSPACE_SURFACE_TOKENS, DERIVED_EDGE_TOKENS, RADIUS_TOKENS, MOTION_TOKENS, TYPE_SCALE_TOKENS, TYPE_REFINEMENT_TOKENS, TYPE_OVERLINE_TOKENS, DENSITY_TOKENS, LAYERING_TOKENS, TYPE_UI_TOKENS, ART_TOKENS } from '../chunk-NETUFI34.js';
|
|
2
|
+
|
|
3
|
+
// src/themes/contrast.ts
|
|
4
|
+
function oklchToLinearRgb(L, C, H) {
|
|
5
|
+
const h = H * Math.PI / 180;
|
|
6
|
+
const a = C * Math.cos(h);
|
|
7
|
+
const b = C * Math.sin(h);
|
|
8
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
9
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
10
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
11
|
+
return [
|
|
12
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
13
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
14
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
function hexToLinearRgb(hex) {
|
|
18
|
+
const m = hex.trim().match(/^#([0-9a-f]{6})$/i);
|
|
19
|
+
if (!m) return null;
|
|
20
|
+
const n = Number.parseInt(m[1], 16);
|
|
21
|
+
const toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
22
|
+
return [
|
|
23
|
+
toLinear((n >> 16 & 255) / 255),
|
|
24
|
+
toLinear((n >> 8 & 255) / 255),
|
|
25
|
+
toLinear((n & 255) / 255)
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
var clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
29
|
+
function linearToSrgb(c) {
|
|
30
|
+
const v = clamp01(c);
|
|
31
|
+
return v <= 31308e-7 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
32
|
+
}
|
|
33
|
+
function parseColorWithAlpha(value) {
|
|
34
|
+
const v = value.trim();
|
|
35
|
+
if (v.startsWith("#")) {
|
|
36
|
+
const rgb2 = hexToLinearRgb(v.length === 9 ? v.slice(0, 7) : v);
|
|
37
|
+
if (!rgb2) return null;
|
|
38
|
+
const alpha2 = v.length === 9 ? Number.parseInt(v.slice(7), 16) / 255 : 1;
|
|
39
|
+
return { rgb: rgb2, alpha: Number.isNaN(alpha2) ? 1 : alpha2 };
|
|
40
|
+
}
|
|
41
|
+
const m = v.match(
|
|
42
|
+
/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+)(%?))?\s*\)/i
|
|
43
|
+
);
|
|
44
|
+
if (!m) return null;
|
|
45
|
+
const rgb = oklchToLinearRgb(
|
|
46
|
+
m[2] === "%" ? Number(m[1]) / 100 : Number(m[1]),
|
|
47
|
+
Number(m[3]),
|
|
48
|
+
Number(m[4])
|
|
49
|
+
);
|
|
50
|
+
const alpha = m[5] === void 0 ? 1 : m[6] === "%" ? Number(m[5]) / 100 : Number(m[5]);
|
|
51
|
+
return { rgb, alpha };
|
|
52
|
+
}
|
|
53
|
+
function luminance([r, g, b]) {
|
|
54
|
+
return 0.2126 * clamp01(r) + 0.7152 * clamp01(g) + 0.0722 * clamp01(b);
|
|
55
|
+
}
|
|
56
|
+
function contrastRatio(a, b) {
|
|
57
|
+
const [l1, l2] = [luminance(a), luminance(b)];
|
|
58
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
59
|
+
}
|
|
60
|
+
var CONTRAST_FLOOR = 4.5;
|
|
61
|
+
var toSrgb = linearToSrgb;
|
|
62
|
+
function apcaLc(ink, surface) {
|
|
63
|
+
const Y = (rgb) => {
|
|
64
|
+
const [r, g, b] = rgb.map(toSrgb);
|
|
65
|
+
const y = 0.2126729 * r ** 2.4 + 0.7151522 * g ** 2.4 + 0.072175 * b ** 2.4;
|
|
66
|
+
return y < 0.022 ? y + (0.022 - y) ** 1.414 : y;
|
|
67
|
+
};
|
|
68
|
+
const Yt = Y(ink);
|
|
69
|
+
const Yb = Y(surface);
|
|
70
|
+
if (Math.abs(Yb - Yt) < 5e-4) return 0;
|
|
71
|
+
let out;
|
|
72
|
+
if (Yb > Yt) {
|
|
73
|
+
const s = (Yb ** 0.56 - Yt ** 0.57) * 1.14;
|
|
74
|
+
out = s < 0.035 ? 0 : s - 0.027;
|
|
75
|
+
} else {
|
|
76
|
+
const s = (Yb ** 0.65 - Yt ** 0.62) * 1.14;
|
|
77
|
+
out = s > -0.035 ? 0 : s + 0.027;
|
|
78
|
+
}
|
|
79
|
+
return out * 100;
|
|
80
|
+
}
|
|
81
|
+
var PERCEPTUAL_FLOOR = 60;
|
|
82
|
+
var GAMUT_EPSILON = 1e-4;
|
|
83
|
+
function isInSrgbGamut(rgb, epsilon = GAMUT_EPSILON) {
|
|
84
|
+
return rgb.every((c) => c >= -epsilon && c <= 1 + epsilon);
|
|
85
|
+
}
|
|
86
|
+
var linearOf = ({ l, c, h }) => oklchToLinearRgb(l, c, h);
|
|
87
|
+
function maxChromaAt(l, h) {
|
|
88
|
+
if (l <= 0 || l >= 1) return 0;
|
|
89
|
+
let lo = 0;
|
|
90
|
+
let hi = 0.4;
|
|
91
|
+
for (let i = 0; i < 24; i++) {
|
|
92
|
+
const mid = (lo + hi) / 2;
|
|
93
|
+
if (isInSrgbGamut(oklchToLinearRgb(l, mid, h))) lo = mid;
|
|
94
|
+
else hi = mid;
|
|
95
|
+
}
|
|
96
|
+
return lo;
|
|
97
|
+
}
|
|
98
|
+
function labelContrast(ink, fill) {
|
|
99
|
+
const a = linearOf(ink);
|
|
100
|
+
const b = linearOf(fill);
|
|
101
|
+
const ratio = contrastRatio(a, b);
|
|
102
|
+
const lc = apcaLc(a, b);
|
|
103
|
+
return { ratio, lc, reads: ratio >= CONTRAST_FLOOR && Math.abs(lc) >= PERCEPTUAL_FLOOR };
|
|
104
|
+
}
|
|
105
|
+
function inkFor(fill, inks) {
|
|
106
|
+
const rank = (ink) => {
|
|
107
|
+
const v = labelContrast(ink, fill);
|
|
108
|
+
return (v.reads ? 2 : v.ratio >= CONTRAST_FLOOR ? 1 : 0) * 1e3 + Math.abs(v.lc);
|
|
109
|
+
};
|
|
110
|
+
return inks.reduce((best, ink) => rank(ink) > rank(best) ? ink : best);
|
|
111
|
+
}
|
|
112
|
+
var FILL_GAMUT_SAFETY = 0.98;
|
|
113
|
+
function fitFillToInk(fill, ink) {
|
|
114
|
+
if (labelContrast(ink, fill).reads) return fill;
|
|
115
|
+
const step = ink.l > fill.l ? -5e-3 : 5e-3;
|
|
116
|
+
let candidate = fill;
|
|
117
|
+
for (let l = fill.l + step; l > 0.05 && l < 0.98; l += step) {
|
|
118
|
+
candidate = { ...fill, l, c: Math.min(fill.c, maxChromaAt(l, fill.h) * FILL_GAMUT_SAFETY) };
|
|
119
|
+
if (labelContrast(ink, candidate).reads) return candidate;
|
|
120
|
+
}
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
function inkPolarity(color, inks) {
|
|
124
|
+
const cost = (ink) => {
|
|
125
|
+
const fitted = fitFillToInk(color, ink);
|
|
126
|
+
const v = labelContrast(ink, fitted);
|
|
127
|
+
return (v.reads ? 0 : 10) + Math.abs(fitted.l - color.l) - Math.abs(v.lc) / 1e4;
|
|
128
|
+
};
|
|
129
|
+
return inks.reduce((best, ink) => cost(ink) < cost(best) ? ink : best);
|
|
130
|
+
}
|
|
131
|
+
function linearRgbToOklch([r, g, b]) {
|
|
132
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
133
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
134
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
135
|
+
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
136
|
+
const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
137
|
+
const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
138
|
+
const c = Math.hypot(A, B);
|
|
139
|
+
return { l: L, c, h: c < 1e-4 ? 0 : (Math.atan2(B, A) * 180 / Math.PI + 360) % 360 };
|
|
140
|
+
}
|
|
141
|
+
var formatOklchColor = ({ l, c, h }) => `oklch(${(l * 100).toFixed(1)}% ${c.toFixed(3)} ${h.toFixed(1)})`;
|
|
142
|
+
var BRAND_FILLS = ["primary", "sidebar-primary", "highlight"];
|
|
143
|
+
var STATUS_FILLS = ["destructive", "success", "tone-warning", "tone-info"];
|
|
144
|
+
var LABELLED_FILLS = [...BRAND_FILLS, ...STATUS_FILLS];
|
|
145
|
+
var LABEL_CONTRAST_RULE = "label-contrast";
|
|
146
|
+
function checkLabelledFills(tokens) {
|
|
147
|
+
const findings = [];
|
|
148
|
+
const unmeasured = [];
|
|
149
|
+
let measured = 0;
|
|
150
|
+
const read = (value) => {
|
|
151
|
+
const parsed = value ? parseColorWithAlpha(value) : null;
|
|
152
|
+
return parsed && parsed.alpha >= 1 ? linearRgbToOklch(parsed.rgb) : null;
|
|
153
|
+
};
|
|
154
|
+
for (const mode of ["light", "dark"]) {
|
|
155
|
+
for (const fill of LABELLED_FILLS) {
|
|
156
|
+
const rawFill = tokens[mode][fill];
|
|
157
|
+
const rawInk = tokens[mode][`${fill}-foreground`];
|
|
158
|
+
if (!rawFill && !rawInk) continue;
|
|
159
|
+
const fillColor = read(rawFill);
|
|
160
|
+
const ink = read(rawInk);
|
|
161
|
+
if (!fillColor || !ink) {
|
|
162
|
+
unmeasured.push(`${mode}.${fill}`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
measured++;
|
|
166
|
+
const verdict = labelContrast(ink, fillColor);
|
|
167
|
+
if (verdict.reads) continue;
|
|
168
|
+
const opposite = ink.l > 0.5 ? { l: 0.2, c: 0.03, h: fillColor.h } : { l: 0.985, c: 0.01, h: fillColor.h };
|
|
169
|
+
const reference = read(tokens.light[fill]) ?? fillColor;
|
|
170
|
+
const chosen = inkPolarity(reference, [ink, { ...opposite, h: reference.h }]);
|
|
171
|
+
const nextInk = chosen === ink ? ink : opposite;
|
|
172
|
+
findings.push({
|
|
173
|
+
mode,
|
|
174
|
+
fill,
|
|
175
|
+
ratio: verdict.ratio,
|
|
176
|
+
lc: verdict.lc,
|
|
177
|
+
suggestion: { fill: formatOklchColor(fitFillToInk(fillColor, nextInk)), ink: formatOklchColor(nextInk) }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { findings, measured, unmeasured };
|
|
182
|
+
}
|
|
2
183
|
|
|
3
184
|
// src/presets/index.ts
|
|
4
185
|
var PRESET_TOKEN_NAMES = [.../* @__PURE__ */ new Set([
|
|
5
186
|
...SEMANTIC_TOKENS,
|
|
6
187
|
...WORKSPACE_SURFACE_TOKENS.map((name) => `theme-${name}`),
|
|
7
188
|
...DERIVED_EDGE_TOKENS.map((name) => `theme-${name}`),
|
|
189
|
+
// Art direction: optional hooks — a preset without them paints the default art.
|
|
190
|
+
...ART_TOKENS.map((name) => `theme-${name}`),
|
|
8
191
|
...WORKSPACE_SURFACE_TOKENS,
|
|
9
192
|
...DERIVED_EDGE_TOKENS,
|
|
10
193
|
...RADIUS_TOKENS,
|
|
@@ -38,7 +221,31 @@ var tokenNames = new Set(PRESET_TOKEN_NAMES);
|
|
|
38
221
|
var record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
39
222
|
var text = (value) => typeof value === "string" && value.trim().length > 0;
|
|
40
223
|
var safeCss = (value) => value.length <= 2048 && !/[;{}<>\\@]|\/\*|\*\/|!important|url\s*\(|expression\s*\(|REPLACE/i.test(value);
|
|
41
|
-
|
|
224
|
+
var STATUS_TEXT_FROM_FILL = {
|
|
225
|
+
"destructive-text": "destructive",
|
|
226
|
+
"success-text": "success",
|
|
227
|
+
"tone-warning-text": "tone-warning",
|
|
228
|
+
"tone-info-text": "tone-info"
|
|
229
|
+
};
|
|
230
|
+
function upgradePreset(input) {
|
|
231
|
+
if (!record(input) || !record(input.tokens)) return input;
|
|
232
|
+
const tokens = input.tokens;
|
|
233
|
+
const shared = record(tokens.shared) ? tokens.shared : {};
|
|
234
|
+
const upgraded = { ...tokens };
|
|
235
|
+
for (const mode of ["light", "dark"]) {
|
|
236
|
+
const values = tokens[mode];
|
|
237
|
+
if (!record(values)) continue;
|
|
238
|
+
const next = { ...values };
|
|
239
|
+
for (const [name, fill] of Object.entries(STATUS_TEXT_FROM_FILL)) {
|
|
240
|
+
const value = values[fill] ?? shared[fill];
|
|
241
|
+
if (!text(values[name]) && !text(shared[name]) && text(value)) next[name] = value;
|
|
242
|
+
}
|
|
243
|
+
upgraded[mode] = next;
|
|
244
|
+
}
|
|
245
|
+
return { ...input, tokens: upgraded };
|
|
246
|
+
}
|
|
247
|
+
function validatePreset(raw) {
|
|
248
|
+
const input = upgradePreset(raw);
|
|
42
249
|
if (!record(input)) return ["Preset must be a JSON object."];
|
|
43
250
|
const issues = [];
|
|
44
251
|
if (input.schemaVersion !== 1) issues.push("Unsupported preset schemaVersion; expected 1.");
|
|
@@ -58,6 +265,13 @@ function validatePreset(input) {
|
|
|
58
265
|
if (!record(item) || !text(item.source) || !text(item.observation) || !["measured", "estimated", "derived", "user-edited"].includes(String(item.method))) issues.push(`Invalid reference observation ${index + 1}.`);
|
|
59
266
|
});
|
|
60
267
|
}
|
|
268
|
+
if (input.exceptions !== void 0) {
|
|
269
|
+
if (!Array.isArray(input.exceptions) || input.exceptions.length > 50) issues.push("exceptions must be an array of at most 50 entries.");
|
|
270
|
+
else input.exceptions.forEach((item, index) => {
|
|
271
|
+
if (!record(item) || item.rule !== LABEL_CONTRAST_RULE || !["light", "dark"].includes(String(item.mode)) || !LABELLED_FILLS.includes(item.token) || !text(item.reason) || item.reason.length > 500)
|
|
272
|
+
issues.push(`Invalid exception ${index + 1}: needs rule "${LABEL_CONTRAST_RULE}", mode light|dark, token (${LABELLED_FILLS.join(", ")}) and a reason.`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
61
275
|
if (!record(input.tokens)) return [...issues, "tokens must contain shared, light, and dark objects."];
|
|
62
276
|
const groups = input.tokens;
|
|
63
277
|
for (const group of ["shared", "light", "dark"]) {
|
|
@@ -92,10 +306,10 @@ function validatePreset(input) {
|
|
|
92
306
|
}
|
|
93
307
|
return issues;
|
|
94
308
|
}
|
|
95
|
-
function parsePreset(
|
|
96
|
-
const issues = validatePreset(
|
|
309
|
+
function parsePreset(raw) {
|
|
310
|
+
const issues = validatePreset(raw);
|
|
97
311
|
if (issues.length) throw new Error(issues.join("\n"));
|
|
98
|
-
return
|
|
312
|
+
return upgradePreset(raw);
|
|
99
313
|
}
|
|
100
314
|
function presetManifest(input) {
|
|
101
315
|
const preset = parsePreset(input);
|
|
@@ -122,5 +336,24 @@ ${declarations(preset.tokens.light)}
|
|
|
122
336
|
}
|
|
123
337
|
`;
|
|
124
338
|
}
|
|
339
|
+
function presetLabelContrast(input) {
|
|
340
|
+
const preset = parsePreset(input);
|
|
341
|
+
const values = (mode) => {
|
|
342
|
+
const all = { ...preset.tokens.shared, ...preset.tokens[mode] };
|
|
343
|
+
const resolve = (value, depth = 0) => depth > 10 ? value : value.replace(/^var\(\s*--([\w-]+)\s*\)$/, (m, name) => all[name] ? resolve(all[name], depth + 1) : m);
|
|
344
|
+
return Object.fromEntries(Object.entries(all).map(([name, value]) => [name, resolve(value)]));
|
|
345
|
+
};
|
|
346
|
+
const { findings, measured, unmeasured } = checkLabelledFills({ light: values("light"), dark: values("dark") });
|
|
347
|
+
const reasonFor = (f) => preset.exceptions?.find((e) => e.rule === LABEL_CONTRAST_RULE && e.mode === f.mode && e.token === f.fill)?.reason;
|
|
348
|
+
return {
|
|
349
|
+
open: findings.filter((f) => !reasonFor(f)),
|
|
350
|
+
accepted: findings.flatMap((f) => {
|
|
351
|
+
const reason = reasonFor(f);
|
|
352
|
+
return reason ? [{ ...f, reason }] : [];
|
|
353
|
+
}),
|
|
354
|
+
measured,
|
|
355
|
+
unmeasured
|
|
356
|
+
};
|
|
357
|
+
}
|
|
125
358
|
|
|
126
|
-
export { PRESET_TOKEN_NAMES, parsePreset, presetCss, presetManifest, validatePreset };
|
|
359
|
+
export { BRAND_FILLS, LABELLED_FILLS, LABEL_CONTRAST_RULE, PRESET_TOKEN_NAMES, STATUS_FILLS, checkLabelledFills, fitFillToInk, inkFor, inkPolarity, labelContrast, parsePreset, presetCss, presetLabelContrast, presetManifest, validatePreset };
|
package/dist/styles.css
CHANGED
|
@@ -109,6 +109,39 @@
|
|
|
109
109
|
--color-tone-edition-foreground: var(--tone-edition-foreground);
|
|
110
110
|
--color-tone-warning-foreground: var(--tone-warning-foreground);
|
|
111
111
|
--color-tone-info-foreground: var(--tone-info-foreground);
|
|
112
|
+
/* Status text: a status colour drawn on a surface. Tailwind resolves each
|
|
113
|
+
property's own namespace before --color-*, so `text-destructive`,
|
|
114
|
+
`border-destructive`, `ring-destructive` (and outline, decoration, fill,
|
|
115
|
+
stroke) read --destructive-text while `bg-destructive` keeps the fill.
|
|
116
|
+
Same class names; the value fits the job. */
|
|
117
|
+
--text-color-destructive: var(--destructive-text);
|
|
118
|
+
--border-color-destructive: var(--destructive-text);
|
|
119
|
+
--ring-color-destructive: var(--destructive-text);
|
|
120
|
+
--outline-color-destructive: var(--destructive-text);
|
|
121
|
+
--text-decoration-color-destructive: var(--destructive-text);
|
|
122
|
+
--fill-destructive: var(--destructive-text);
|
|
123
|
+
--stroke-destructive: var(--destructive-text);
|
|
124
|
+
--text-color-success: var(--success-text);
|
|
125
|
+
--border-color-success: var(--success-text);
|
|
126
|
+
--ring-color-success: var(--success-text);
|
|
127
|
+
--outline-color-success: var(--success-text);
|
|
128
|
+
--text-decoration-color-success: var(--success-text);
|
|
129
|
+
--fill-success: var(--success-text);
|
|
130
|
+
--stroke-success: var(--success-text);
|
|
131
|
+
--text-color-tone-warning: var(--tone-warning-text);
|
|
132
|
+
--border-color-tone-warning: var(--tone-warning-text);
|
|
133
|
+
--ring-color-tone-warning: var(--tone-warning-text);
|
|
134
|
+
--outline-color-tone-warning: var(--tone-warning-text);
|
|
135
|
+
--text-decoration-color-tone-warning: var(--tone-warning-text);
|
|
136
|
+
--fill-tone-warning: var(--tone-warning-text);
|
|
137
|
+
--stroke-tone-warning: var(--tone-warning-text);
|
|
138
|
+
--text-color-tone-info: var(--tone-info-text);
|
|
139
|
+
--border-color-tone-info: var(--tone-info-text);
|
|
140
|
+
--ring-color-tone-info: var(--tone-info-text);
|
|
141
|
+
--outline-color-tone-info: var(--tone-info-text);
|
|
142
|
+
--text-decoration-color-tone-info: var(--tone-info-text);
|
|
143
|
+
--fill-tone-info: var(--tone-info-text);
|
|
144
|
+
--stroke-tone-info: var(--tone-info-text);
|
|
112
145
|
--color-highlight-foreground: var(--highlight-foreground);
|
|
113
146
|
|
|
114
147
|
/* Overlay — fixed dark scrim/media surface + its light ink (bg-overlay,
|
|
@@ -352,6 +385,23 @@
|
|
|
352
385
|
are hard 3–12px drops); on a 20px knob inside its track that drop escaped
|
|
353
386
|
the track. A knob only sits on its rail, so it keeps one small shadow. */
|
|
354
387
|
--knob-shadow: var(--theme-knob-shadow, 0 1px 2px 0 rgb(0 0 0 / 0.05));
|
|
388
|
+
|
|
389
|
+
/* Art direction for generated imagery (GradientArt; see ART_TOKENS in
|
|
390
|
+
themes/_contract.ts). The default paints the four decorative tones,
|
|
391
|
+
lifted from their text strength to an art strength, glowing over the brand
|
|
392
|
+
steeped in the first tone; a theme overrides any of it through the
|
|
393
|
+
--theme-art-* hooks. Declared here, on every scope, so a .dark island or a
|
|
394
|
+
preset-scoped preview paints from its own tokens. */
|
|
395
|
+
--art-1: var(--theme-art-1, oklch(from var(--tone-info) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h));
|
|
396
|
+
--art-2: var(--theme-art-2, oklch(from var(--tone-collectible) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h));
|
|
397
|
+
--art-3: var(--theme-art-3, oklch(from var(--tone-edition) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h));
|
|
398
|
+
--art-4: var(--theme-art-4, oklch(from var(--tone-standard) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h));
|
|
399
|
+
--art-5: var(--theme-art-5, transparent);
|
|
400
|
+
--art-6: var(--theme-art-6, transparent);
|
|
401
|
+
--art-ground: var(--theme-art-ground, color-mix(in oklab, var(--primary) 55%, oklch(from var(--tone-info) 0.3 c h)));
|
|
402
|
+
--art-warp: var(--theme-art-warp, 0.8);
|
|
403
|
+
--art-softness: var(--theme-art-softness, 0.88);
|
|
404
|
+
--art-grain: var(--theme-art-grain, 0);
|
|
355
405
|
}
|
|
356
406
|
|
|
357
407
|
/* ==========================================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { T as ThemeManifest } from './_contract-
|
|
3
|
+
import { T as ThemeManifest } from './_contract-C2M1SjBf.js';
|
|
4
4
|
|
|
5
5
|
declare const SABLE_THEME_ATTRIBUTE: "data-sable-theme";
|
|
6
6
|
declare const DEFAULT_THEME_ID: string;
|