@nanisoft/prism-ui 0.5.1 → 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/dist/.tsbuildinfo +1 -1
- package/dist/blocks/cta-01/cta.d.ts +66 -6
- package/dist/blocks/cta-01/cta.d.ts.map +1 -1
- package/dist/blocks/cta-01/cta.js +8 -3
- package/dist/blocks/cta-01/index.d.ts +1 -0
- package/dist/blocks/cta-01/index.d.ts.map +1 -1
- package/dist/blocks/feature-grid-01/feature-grid.d.ts +57 -6
- package/dist/blocks/feature-grid-01/feature-grid.d.ts.map +1 -1
- package/dist/blocks/feature-grid-01/feature-grid.js +12 -2
- package/dist/blocks/feature-grid-01/index.d.ts +1 -0
- package/dist/blocks/feature-grid-01/index.d.ts.map +1 -1
- package/dist/blocks/hero-01/hero.d.ts +30 -1
- package/dist/blocks/hero-01/hero.d.ts.map +1 -1
- package/dist/blocks/hero-01/hero.js +14 -2
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/index.d.ts.map +1 -1
- package/dist/blocks/index.js +9 -0
- package/dist/blocks/instrument-panel-01/index.d.ts +3 -0
- package/dist/blocks/instrument-panel-01/index.d.ts.map +1 -0
- package/dist/blocks/instrument-panel-01/index.js +1 -0
- package/dist/blocks/instrument-panel-01/instrument-panel.d.ts +114 -0
- package/dist/blocks/instrument-panel-01/instrument-panel.d.ts.map +1 -0
- package/dist/blocks/instrument-panel-01/instrument-panel.js +64 -0
- package/dist/blocks/logo-strip-01/index.d.ts +3 -0
- package/dist/blocks/logo-strip-01/index.d.ts.map +1 -0
- package/dist/blocks/logo-strip-01/index.js +1 -0
- package/dist/blocks/logo-strip-01/logo-strip.d.ts +84 -0
- package/dist/blocks/logo-strip-01/logo-strip.d.ts.map +1 -0
- package/dist/blocks/logo-strip-01/logo-strip.js +40 -0
- package/dist/blocks/note-grid-01/index.d.ts +3 -0
- package/dist/blocks/note-grid-01/index.d.ts.map +1 -0
- package/dist/blocks/note-grid-01/index.js +1 -0
- package/dist/blocks/note-grid-01/note-grid.d.ts +79 -0
- package/dist/blocks/note-grid-01/note-grid.d.ts.map +1 -0
- package/dist/blocks/note-grid-01/note-grid.js +37 -0
- package/dist/blocks/pricing-01/pricing.js +1 -1
- package/dist/blocks/process-rail-01/index.d.ts +3 -0
- package/dist/blocks/process-rail-01/index.d.ts.map +1 -0
- package/dist/blocks/process-rail-01/index.js +1 -0
- package/dist/blocks/process-rail-01/process-rail.d.ts +99 -0
- package/dist/blocks/process-rail-01/process-rail.d.ts.map +1 -0
- package/dist/blocks/process-rail-01/process-rail.js +58 -0
- package/dist/blocks/product-grid-01/index.d.ts +3 -0
- package/dist/blocks/product-grid-01/index.d.ts.map +1 -0
- package/dist/blocks/product-grid-01/index.js +1 -0
- package/dist/blocks/product-grid-01/product-grid.d.ts +114 -0
- package/dist/blocks/product-grid-01/product-grid.d.ts.map +1 -0
- package/dist/blocks/product-grid-01/product-grid.js +51 -0
- package/dist/blocks/site-footer/index.d.ts +3 -0
- package/dist/blocks/site-footer/index.d.ts.map +1 -0
- package/dist/blocks/site-footer/index.js +1 -0
- package/dist/blocks/site-footer/site-footer.d.ts +135 -0
- package/dist/blocks/site-footer/site-footer.d.ts.map +1 -0
- package/dist/blocks/site-footer/site-footer.js +41 -0
- package/dist/blocks/site-header/index.d.ts +3 -0
- package/dist/blocks/site-header/index.d.ts.map +1 -0
- package/dist/blocks/site-header/index.js +1 -0
- package/dist/blocks/site-header/site-header.d.ts +142 -0
- package/dist/blocks/site-header/site-header.d.ts.map +1 -0
- package/dist/blocks/site-header/site-header.js +46 -0
- package/dist/blocks/stack-grid-01/index.d.ts +3 -0
- package/dist/blocks/stack-grid-01/index.d.ts.map +1 -0
- package/dist/blocks/stack-grid-01/index.js +1 -0
- package/dist/blocks/stack-grid-01/stack-grid.d.ts +118 -0
- package/dist/blocks/stack-grid-01/stack-grid.d.ts.map +1 -0
- package/dist/blocks/stack-grid-01/stack-grid.js +49 -0
- package/dist/blocks/status-ledger-01/index.d.ts +3 -0
- package/dist/blocks/status-ledger-01/index.d.ts.map +1 -0
- package/dist/blocks/status-ledger-01/index.js +1 -0
- package/dist/blocks/status-ledger-01/status-ledger.d.ts +131 -0
- package/dist/blocks/status-ledger-01/status-ledger.d.ts.map +1 -0
- package/dist/blocks/status-ledger-01/status-ledger.js +80 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +170 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +6 -0
- package/dist/components/ui/card.d.ts +25 -0
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/card.js +25 -0
- package/dist/components/ui/cta-link.d.ts +45 -0
- package/dist/components/ui/cta-link.d.ts.map +1 -0
- package/dist/components/ui/cta-link.js +81 -0
- package/dist/components/ui/diagram.d.ts +154 -0
- package/dist/components/ui/diagram.d.ts.map +1 -0
- package/dist/components/ui/diagram.js +150 -0
- package/dist/components/ui/fact-list.d.ts +90 -0
- package/dist/components/ui/fact-list.d.ts.map +1 -0
- package/dist/components/ui/fact-list.js +38 -0
- package/dist/components/ui/product-mark.d.ts +115 -0
- package/dist/components/ui/product-mark.d.ts.map +1 -0
- package/dist/components/ui/product-mark.js +82 -0
- package/dist/components/ui/product-switcher.d.ts +108 -0
- package/dist/components/ui/product-switcher.d.ts.map +1 -0
- package/dist/components/ui/product-switcher.js +47 -0
- package/dist/components/ui/prose.d.ts +88 -0
- package/dist/components/ui/prose.d.ts.map +1 -0
- package/dist/components/ui/prose.js +53 -0
- package/dist/components/ui/section.d.ts +24 -1
- package/dist/components/ui/section.d.ts.map +1 -1
- package/dist/components/ui/section.js +11 -2
- package/dist/components/ui/slider.d.ts.map +1 -1
- package/dist/components/ui/slider.js +7 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/pages/blog-post-page/index.d.ts +174 -0
- package/dist/pages/blog-post-page/index.d.ts.map +1 -0
- package/dist/pages/blog-post-page/index.js +49 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/pages/index.js +2 -0
- package/dist/pages/not-found-page/index.d.ts +112 -0
- package/dist/pages/not-found-page/index.d.ts.map +1 -0
- package/dist/pages/not-found-page/index.js +46 -0
- package/dist/provider/provider.d.ts +5 -0
- package/dist/provider/provider.d.ts.map +1 -1
- package/dist/provider/provider.js +129 -38
- package/dist/provider/theme-script.d.ts +41 -3
- package/dist/provider/theme-script.d.ts.map +1 -1
- package/dist/provider/theme-script.js +68 -17
- package/dist/styles.css +493 -12
- package/dist/theming/index.d.ts +132 -2
- package/dist/theming/index.d.ts.map +1 -1
- package/dist/theming/index.js +127 -1
- package/package.json +14 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
/*
|
|
5
|
+
* Why this is a second Component rather than an `as` or `href` prop on `Button`.
|
|
6
|
+
* The design system has already answered this twice, in the two places a trail
|
|
7
|
+
* of links and a list of page links each needed both a non-link slot and a link
|
|
8
|
+
* slot: `BreadcrumbItem` renders an `<li>` and `BreadcrumbLink` renders an
|
|
9
|
+
* `<a>` (`src/components/ui/breadcrumb.tsx`), and `PaginationItem` renders an
|
|
10
|
+
* `<li>` and `PaginationLink` renders an `<a>`
|
|
11
|
+
* (`src/components/ui/pagination.tsx`). Both wrote a second export rather than
|
|
12
|
+
* widening one element into a prop, and both are the shape this Component takes.
|
|
13
|
+
* A polymorphic `as` would make the rendered element a runtime value that only
|
|
14
|
+
* the reader of the call site knows, which is exactly the disagreement between a
|
|
15
|
+
* type and a screen this ticket closes. It also costs nothing: both halves of
|
|
16
|
+
* that pair are server components, so the swap adds no client JavaScript.
|
|
17
|
+
*
|
|
18
|
+
* The recipe is written here rather than imported from `button.tsx`, because
|
|
19
|
+
* `buttonVariants` is module-internal and DESIGN.md keeps a raw variant map off
|
|
20
|
+
* the export surface: "There is no variant recipe on the surface." The base
|
|
21
|
+
* string and every shared variant below are therefore kept deliberately
|
|
22
|
+
* identical to `buttonVariants`, so swapping `Button` for `CtaLink` changes the
|
|
23
|
+
* element and the destination and nothing else on screen. If one recipe moves,
|
|
24
|
+
* the other moves with it.
|
|
25
|
+
*/
|
|
26
|
+
const ctaLinkVariants = cva(
|
|
27
|
+
// The focus ring is `ring-ring` at full strength, for the reason
|
|
28
|
+
// `button.tsx` states at length: half alpha composites to between 1.14:1 and
|
|
29
|
+
// 2.74:1 against every surface in all six themes and clears 3:1 in none of
|
|
30
|
+
// them, which fails WCAG 1.4.11 on the one indicator a keyboard user has.
|
|
31
|
+
//
|
|
32
|
+
// Two utilities are dropped from the button base, and each is dropped for a
|
|
33
|
+
// reason rather than by omission. `aria-invalid:*` is a form-control state and
|
|
34
|
+
// an anchor has no invalid state. `disabled:*` is likewise a button state: an
|
|
35
|
+
// anchor with a destination always navigates, and a control that should not
|
|
36
|
+
// navigate is not a call to action.
|
|
37
|
+
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium outline-none transition-[color,box-shadow,background-color] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring focus-visible:ring-[3px]", {
|
|
38
|
+
variants: {
|
|
39
|
+
variant: {
|
|
40
|
+
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
|
41
|
+
outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
|
|
42
|
+
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
|
43
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
44
|
+
},
|
|
45
|
+
// The desktop metrics and the coarse-pointer 44px step are `Button`'s, for
|
|
46
|
+
// the reason `button.tsx` states: the target grows with the input method
|
|
47
|
+
// and not with the viewport, so a phone-sized metric never ships to a
|
|
48
|
+
// mouse. An `icon` size is absent because a call to action carries a label.
|
|
49
|
+
size: {
|
|
50
|
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3 pointer-coarse:h-11 pointer-coarse:min-w-11 pointer-coarse:px-5 pointer-coarse:has-[>svg]:px-4',
|
|
51
|
+
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5 pointer-coarse:h-11 pointer-coarse:min-w-11',
|
|
52
|
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4 pointer-coarse:h-11',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultVariants: {
|
|
56
|
+
variant: 'default',
|
|
57
|
+
size: 'default',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* The call to action that goes somewhere.
|
|
62
|
+
*
|
|
63
|
+
* Renders a native `<a>`, so assistive technology announces a link and the
|
|
64
|
+
* browser's own link affordances all work: a status bar showing the destination,
|
|
65
|
+
* a context menu to copy it, middle-click to open a new tab. `Button` cannot
|
|
66
|
+
* offer any of that, and a call to action that is a button is announced as a
|
|
67
|
+
* command that does nothing.
|
|
68
|
+
*
|
|
69
|
+
* It is a Component in its own right rather than a mode of `Button`, which is the
|
|
70
|
+
* answer `BreadcrumbLink` and `PaginationLink` already give twice in this
|
|
71
|
+
* package. The two carry the same visual weight as the button they replace, so
|
|
72
|
+
* the only difference on screen is that this one has a destination.
|
|
73
|
+
*
|
|
74
|
+
* When `newTab` is set, `rel` defaults to `noopener noreferrer`, so the opened
|
|
75
|
+
* document cannot reach back through `window.opener` or read the referrer. A
|
|
76
|
+
* consumer that needs a different relationship passes `rel` and it wins.
|
|
77
|
+
*/
|
|
78
|
+
function CtaLink({ className, variant, size, href, newTab = false, rel, ...props }) {
|
|
79
|
+
return (_jsx("a", { "data-slot": "cta-link", href: href, rel: rel ?? (newTab ? 'noopener noreferrer' : undefined), target: newTab ? '_blank' : undefined, className: cn(ctaLinkVariants({ variant, size, className })), ...props }));
|
|
80
|
+
}
|
|
81
|
+
export { CtaLink };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One named thing in a Diagram, at a position the caller chooses.
|
|
3
|
+
*
|
|
4
|
+
* `x` and `y` are in the caller's own coordinate space and are normalised into
|
|
5
|
+
* the Diagram's canvas, so fractions, pixels or any other monotonic scale all
|
|
6
|
+
* draw the same shape. Two nodes given the same position collapse onto one
|
|
7
|
+
* another, so a caller that means a column spreads it rather than repeating a
|
|
8
|
+
* number.
|
|
9
|
+
*
|
|
10
|
+
* `emphasis` marks the one node the drawing is about, and it is the only
|
|
11
|
+
* difference a node may carry: the emphasised node takes the brand ink, which
|
|
12
|
+
* DESIGN.md's Brand Ink Rule names as the colour for something that must read
|
|
13
|
+
* as the brand rather than as body text. Two emphasised nodes are a caller's
|
|
14
|
+
* mistake, not a second emphasis level.
|
|
15
|
+
*/
|
|
16
|
+
export type DiagramNode = {
|
|
17
|
+
/**
|
|
18
|
+
* A key unique within the diagram. Relations name a node by this, and it is
|
|
19
|
+
* carried on the markup as `data-node` so a test can address one node rather
|
|
20
|
+
* than the first one.
|
|
21
|
+
*/
|
|
22
|
+
id: string;
|
|
23
|
+
/**
|
|
24
|
+
* The name printed under the node. It is a label on a picture, so a screen
|
|
25
|
+
* reader does not read it: see `DiagramProps` for what carries the meaning.
|
|
26
|
+
*/
|
|
27
|
+
name: string;
|
|
28
|
+
/** The node's position along the horizontal axis, in the caller's space. */
|
|
29
|
+
x: number;
|
|
30
|
+
/** The node's position along the vertical axis, in the caller's space. */
|
|
31
|
+
y: number;
|
|
32
|
+
/** Marks the node the diagram is about. See the Brand Ink Rule. */
|
|
33
|
+
emphasis?: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One relation between two nodes, with the word that says what it is.
|
|
37
|
+
*
|
|
38
|
+
* A relation is drawn as a straight line between the two node marks, so the
|
|
39
|
+
* geometry a caller lays out is the geometry a reader sees. The label is placed
|
|
40
|
+
* beside the line rather than on it, because a label drawn over a line has to
|
|
41
|
+
* be knocked out with a halo, and a halo has to be filled with the colour of
|
|
42
|
+
* the surface the diagram happens to sit on, which the component is not told.
|
|
43
|
+
*
|
|
44
|
+
* `indirect` draws the line dashed. That is the standing convention for "this
|
|
45
|
+
* holds, but not by this route", and it is chosen over fading the line because
|
|
46
|
+
* a dashed line keeps its contrast while a faded one does not.
|
|
47
|
+
*/
|
|
48
|
+
export type DiagramRelation = {
|
|
49
|
+
/** The `id` of the node the relation leaves. */
|
|
50
|
+
from: string;
|
|
51
|
+
/** The `id` of the node the relation arrives at. */
|
|
52
|
+
to: string;
|
|
53
|
+
/** The word printed beside the line. */
|
|
54
|
+
label: string;
|
|
55
|
+
/** Draws the line dashed, for a relation that holds but not by this route. */
|
|
56
|
+
indirect?: boolean;
|
|
57
|
+
};
|
|
58
|
+
/** What both arms of `DiagramProps` carry. */
|
|
59
|
+
type DiagramFigure = {
|
|
60
|
+
/** The things drawn. At least one, or the Diagram renders an empty canvas. */
|
|
61
|
+
nodes: DiagramNode[];
|
|
62
|
+
/** The relations drawn between them. A relation naming a node that is not in `nodes` is dropped. */
|
|
63
|
+
relations: DiagramRelation[];
|
|
64
|
+
/**
|
|
65
|
+
* Layout only, exactly as on every Component: grid placement and width.
|
|
66
|
+
* Changing a Prism-owned visual property from here is prohibited, and this
|
|
67
|
+
* Component spreads no other prop, so there is no second way to do it.
|
|
68
|
+
*/
|
|
69
|
+
className?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The props a Diagram takes.
|
|
73
|
+
*
|
|
74
|
+
* The name is required unless the drawing is decorative, and it is forbidden
|
|
75
|
+
* when it is. That is a union rather than `label?: string` because only a union
|
|
76
|
+
* lets the type system see the exception: an optional label on one type can
|
|
77
|
+
* only be optional everywhere or required everywhere, and both halves of that
|
|
78
|
+
* are wrong. `Separator` takes the same shape as `decorative`.
|
|
79
|
+
*/
|
|
80
|
+
export type DiagramProps = DiagramFigure & ({
|
|
81
|
+
/**
|
|
82
|
+
* Hides the drawing from assistive technology and drops the name. Use it
|
|
83
|
+
* when the surrounding sentence already says what the diagram shows, and
|
|
84
|
+
* nothing inside the hidden tree is named either: there is no `<title>`
|
|
85
|
+
* here, so there is no name stranded inside an `aria-hidden` subtree.
|
|
86
|
+
*/
|
|
87
|
+
decorative: true;
|
|
88
|
+
label?: never;
|
|
89
|
+
} | {
|
|
90
|
+
/** The name of the drawing, and the only thing a screen reader reads from it. */
|
|
91
|
+
label: string;
|
|
92
|
+
decorative?: false;
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* A diagram of named things and the relations between them.
|
|
96
|
+
*
|
|
97
|
+
* Data in, vector markup out. Every stroke and every fill is a semantic token
|
|
98
|
+
* named as a utility, never a value read from anywhere, so a scoped `data-pack`
|
|
99
|
+
* boundary above the drawing restyles it through the cascade exactly as it
|
|
100
|
+
* restyles a heading. That is the whole reason this is a Component and the
|
|
101
|
+
* reason a canvas was not: a canvas paints pixels it has already resolved, and
|
|
102
|
+
* a resolved value does not move when the pack beneath it does.
|
|
103
|
+
*
|
|
104
|
+
* It is a server Component. It ships no client code, takes no hook, has no mode
|
|
105
|
+
* of its own and reads no context, so a consumer renders it from a server file
|
|
106
|
+
* with no provider mounted. `check-client-budget.mjs` keeps it off the client
|
|
107
|
+
* roster, and a `'use client'` line added here would put it back on that roster.
|
|
108
|
+
*
|
|
109
|
+
* **Accessibility.** A diagram carries meaning, so by default it is one image
|
|
110
|
+
* with a name: `role="img"` and the `label` the caller passes. Everything inside
|
|
111
|
+
* a `role="img"` is presentational, so the node names and relation words are
|
|
112
|
+
* drawn for sighted readers and the surrounding sentence is what a screen reader
|
|
113
|
+
* gets. Pass `decorative` when the sentence already says what the drawing
|
|
114
|
+
* shows, and the whole tree leaves the accessibility tree with nothing named
|
|
115
|
+
* inside it.
|
|
116
|
+
*
|
|
117
|
+
* **Shape.** Nodes are circles and relations are paths. Both are shapes with no
|
|
118
|
+
* radius concept, so no corner radius here is a function of `--radius` and a
|
|
119
|
+
* pack boundary above the drawing moves nothing about its shape. A `<rect>` was
|
|
120
|
+
* rejected for the opposite reason: its corner attribute is a CSS property and
|
|
121
|
+
* does follow a boundary, and no utility pins it.
|
|
122
|
+
*
|
|
123
|
+
* **The node mark's base edges must draw.** Two things go wrong when a shape is
|
|
124
|
+
* left to its defaults, and this file answers both explicitly on every mark.
|
|
125
|
+
*
|
|
126
|
+
* 1. A shape with no `fill` and no `fill: none` paints opaque black, which
|
|
127
|
+
* hides every label behind it. A relation path therefore names `fill-none`
|
|
128
|
+
* and a node circle names a fill, and neither relies on an initial value.
|
|
129
|
+
* 2. A stroke only exists if something draws it. `stroke-width`'s initial
|
|
130
|
+
* value is 1 rather than 0, so the width is not what fails here; what fails
|
|
131
|
+
* is a mark whose stroke colour is `currentColor` on a surface whose text
|
|
132
|
+
* colour is that same surface, which is an invisible edge, or a width
|
|
133
|
+
* utility dropped in an edit. So every mark names an explicit width, an
|
|
134
|
+
* explicit stroke from a token, and never `current` or `currentColor`.
|
|
135
|
+
*
|
|
136
|
+
* A Component whose nodes render as invisible rectangles is worse than no
|
|
137
|
+
* Component, so this is stated here and covered by `test/diagram.test.tsx`.
|
|
138
|
+
*
|
|
139
|
+
* It spreads no other prop. `Separator` and `Kbd` spread a native element's
|
|
140
|
+
* props because there is no other route to them; a Diagram whose entire claim
|
|
141
|
+
* is that every ink is a token would be undermined by a spread that hands a
|
|
142
|
+
* caller `stroke`, so the surface is `nodes`, `relations`, `label`,
|
|
143
|
+
* `decorative` and `className` and nothing else.
|
|
144
|
+
*
|
|
145
|
+
* Positions are the caller's: the Component fits and draws, and it does not lay
|
|
146
|
+
* out a graph, so it cannot know that two labels will not collide. A relation
|
|
147
|
+
* naming a node that is not in `nodes` is dropped, and the count of dropped
|
|
148
|
+
* relations is on the element as `data-unresolved-relations` rather than
|
|
149
|
+
* swallowed, because a line that silently did not draw is the failure this
|
|
150
|
+
* Component exists to make impossible.
|
|
151
|
+
*/
|
|
152
|
+
declare function Diagram({ nodes, relations, label, decorative, className, }: DiagramProps): import("react").JSX.Element;
|
|
153
|
+
export { Diagram };
|
|
154
|
+
//# sourceMappingURL=diagram.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagram.d.ts","sourceRoot":"","sources":["../../../src/components/ui/diagram.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,CAAC,EAAE,MAAM,CAAA;IACT,0EAA0E;IAC1E,CAAC,EAAE,MAAM,CAAA;IACT,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAA;IACV,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,8CAA8C;AAC9C,KAAK,aAAa,GAAG;IACnB,8EAA8E;IAC9E,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,oGAAoG;IACpG,SAAS,EAAE,eAAe,EAAE,CAAA;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CACI;IACE;;;;;OAKG;IACH,UAAU,EAAE,IAAI,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;CACd,GACD;IACE,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,KAAK,CAAA;CACnB,CACJ,CAAA;AAoFH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,iBAAS,OAAO,CAAC,EACf,KAAK,EACL,SAAS,EACT,KAAK,EACL,UAAkB,EAClB,SAAS,GACV,EAAE,YAAY,+BAsFd;AAED,OAAO,EAAE,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
/**
|
|
4
|
+
* The canvas the Diagram draws into, in its own user units.
|
|
5
|
+
*
|
|
6
|
+
* A fixed canvas rather than a viewBox derived from the caller's numbers, for
|
|
7
|
+
* one reason: type inside a scaled SVG is sized in the SVG's own user units, so
|
|
8
|
+
* a viewBox derived from a caller's coordinates would make the label size a
|
|
9
|
+
* function of how large a number the caller happened to use, and the same
|
|
10
|
+
* drawing would come out with 3px labels in one call and 40px labels in another.
|
|
11
|
+
* Fitting the caller's points into a fixed canvas makes the label a constant
|
|
12
|
+
* and makes the drawing a function of the shape rather than of the scale.
|
|
13
|
+
*/
|
|
14
|
+
const CANVAS_WIDTH = 640;
|
|
15
|
+
const CANVAS_HEIGHT = 400;
|
|
16
|
+
/**
|
|
17
|
+
* The band left empty around the fitted points, in user units.
|
|
18
|
+
*
|
|
19
|
+
* It reserves room for what is drawn beside a node rather than for the node: a
|
|
20
|
+
* node mark is 5 units in radius and its name is set about 20 units below it,
|
|
21
|
+
* and a name is wider than the mark. The band is the honest cost of a server
|
|
22
|
+
* component not being able to measure text, and it is why a name that runs long
|
|
23
|
+
* still lands inside the canvas rather than off its edge.
|
|
24
|
+
*/
|
|
25
|
+
const PADDING = 56;
|
|
26
|
+
/** The node mark's radius. A circle, so there is no corner radius for the pack to move. */
|
|
27
|
+
const NODE_RADIUS = 5;
|
|
28
|
+
/**
|
|
29
|
+
* Type sizes, in user units rather than in `rem`.
|
|
30
|
+
*
|
|
31
|
+
* The authored type scale is in `rem` and cannot be used inside a viewBox, where
|
|
32
|
+
* a font size is a coordinate and not a length on the page. These are geometry,
|
|
33
|
+
* the one thing Tailwind stays authoritative for, and they are the only numbers
|
|
34
|
+
* in this file that are not a token.
|
|
35
|
+
*/
|
|
36
|
+
const NODE_NAME_SIZE = 13;
|
|
37
|
+
const NODE_NAME_OFFSET = 20;
|
|
38
|
+
const RELATION_NAME_SIZE = 11;
|
|
39
|
+
/**
|
|
40
|
+
* How far a relation's label sits off its line, in user units. Perpendicular to
|
|
41
|
+
* the line, so the label never sits on the stroke it belongs to.
|
|
42
|
+
*/
|
|
43
|
+
const RELATION_NAME_LIFT = 7;
|
|
44
|
+
/**
|
|
45
|
+
* The caller's points, fitted into the canvas with their aspect ratio kept and
|
|
46
|
+
* the leftover shared out evenly.
|
|
47
|
+
*
|
|
48
|
+
* A caller whose points span one axis only, or who passes a single node, gets
|
|
49
|
+
* the other axis centred rather than a division by zero: the scale is taken
|
|
50
|
+
* over the axes that have an extent, and a diagram with no extent at all keeps
|
|
51
|
+
* the caller's own numbers.
|
|
52
|
+
*/
|
|
53
|
+
function fit(nodes) {
|
|
54
|
+
if (nodes.length === 0)
|
|
55
|
+
return () => ({ x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2 });
|
|
56
|
+
const xs = nodes.map((node) => node.x);
|
|
57
|
+
const ys = nodes.map((node) => node.y);
|
|
58
|
+
const minX = Math.min(...xs);
|
|
59
|
+
const minY = Math.min(...ys);
|
|
60
|
+
const spanX = Math.max(...xs) - minX;
|
|
61
|
+
const spanY = Math.max(...ys) - minY;
|
|
62
|
+
const roomX = CANVAS_WIDTH - PADDING * 2;
|
|
63
|
+
const roomY = CANVAS_HEIGHT - PADDING * 2;
|
|
64
|
+
const scale = Math.min(spanX === 0 ? Number.POSITIVE_INFINITY : roomX / spanX, spanY === 0 ? Number.POSITIVE_INFINITY : roomY / spanY);
|
|
65
|
+
const unit = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
66
|
+
const offsetX = PADDING + (roomX - spanX * unit) / 2 - minX * unit;
|
|
67
|
+
const offsetY = PADDING + (roomY - spanY * unit) / 2 - minY * unit;
|
|
68
|
+
return (node) => ({ x: node.x * unit + offsetX, y: node.y * unit + offsetY });
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A diagram of named things and the relations between them.
|
|
72
|
+
*
|
|
73
|
+
* Data in, vector markup out. Every stroke and every fill is a semantic token
|
|
74
|
+
* named as a utility, never a value read from anywhere, so a scoped `data-pack`
|
|
75
|
+
* boundary above the drawing restyles it through the cascade exactly as it
|
|
76
|
+
* restyles a heading. That is the whole reason this is a Component and the
|
|
77
|
+
* reason a canvas was not: a canvas paints pixels it has already resolved, and
|
|
78
|
+
* a resolved value does not move when the pack beneath it does.
|
|
79
|
+
*
|
|
80
|
+
* It is a server Component. It ships no client code, takes no hook, has no mode
|
|
81
|
+
* of its own and reads no context, so a consumer renders it from a server file
|
|
82
|
+
* with no provider mounted. `check-client-budget.mjs` keeps it off the client
|
|
83
|
+
* roster, and a `'use client'` line added here would put it back on that roster.
|
|
84
|
+
*
|
|
85
|
+
* **Accessibility.** A diagram carries meaning, so by default it is one image
|
|
86
|
+
* with a name: `role="img"` and the `label` the caller passes. Everything inside
|
|
87
|
+
* a `role="img"` is presentational, so the node names and relation words are
|
|
88
|
+
* drawn for sighted readers and the surrounding sentence is what a screen reader
|
|
89
|
+
* gets. Pass `decorative` when the sentence already says what the drawing
|
|
90
|
+
* shows, and the whole tree leaves the accessibility tree with nothing named
|
|
91
|
+
* inside it.
|
|
92
|
+
*
|
|
93
|
+
* **Shape.** Nodes are circles and relations are paths. Both are shapes with no
|
|
94
|
+
* radius concept, so no corner radius here is a function of `--radius` and a
|
|
95
|
+
* pack boundary above the drawing moves nothing about its shape. A `<rect>` was
|
|
96
|
+
* rejected for the opposite reason: its corner attribute is a CSS property and
|
|
97
|
+
* does follow a boundary, and no utility pins it.
|
|
98
|
+
*
|
|
99
|
+
* **The node mark's base edges must draw.** Two things go wrong when a shape is
|
|
100
|
+
* left to its defaults, and this file answers both explicitly on every mark.
|
|
101
|
+
*
|
|
102
|
+
* 1. A shape with no `fill` and no `fill: none` paints opaque black, which
|
|
103
|
+
* hides every label behind it. A relation path therefore names `fill-none`
|
|
104
|
+
* and a node circle names a fill, and neither relies on an initial value.
|
|
105
|
+
* 2. A stroke only exists if something draws it. `stroke-width`'s initial
|
|
106
|
+
* value is 1 rather than 0, so the width is not what fails here; what fails
|
|
107
|
+
* is a mark whose stroke colour is `currentColor` on a surface whose text
|
|
108
|
+
* colour is that same surface, which is an invisible edge, or a width
|
|
109
|
+
* utility dropped in an edit. So every mark names an explicit width, an
|
|
110
|
+
* explicit stroke from a token, and never `current` or `currentColor`.
|
|
111
|
+
*
|
|
112
|
+
* A Component whose nodes render as invisible rectangles is worse than no
|
|
113
|
+
* Component, so this is stated here and covered by `test/diagram.test.tsx`.
|
|
114
|
+
*
|
|
115
|
+
* It spreads no other prop. `Separator` and `Kbd` spread a native element's
|
|
116
|
+
* props because there is no other route to them; a Diagram whose entire claim
|
|
117
|
+
* is that every ink is a token would be undermined by a spread that hands a
|
|
118
|
+
* caller `stroke`, so the surface is `nodes`, `relations`, `label`,
|
|
119
|
+
* `decorative` and `className` and nothing else.
|
|
120
|
+
*
|
|
121
|
+
* Positions are the caller's: the Component fits and draws, and it does not lay
|
|
122
|
+
* out a graph, so it cannot know that two labels will not collide. A relation
|
|
123
|
+
* naming a node that is not in `nodes` is dropped, and the count of dropped
|
|
124
|
+
* relations is on the element as `data-unresolved-relations` rather than
|
|
125
|
+
* swallowed, because a line that silently did not draw is the failure this
|
|
126
|
+
* Component exists to make impossible.
|
|
127
|
+
*/
|
|
128
|
+
function Diagram({ nodes, relations, label, decorative = false, className, }) {
|
|
129
|
+
const place = fit(nodes);
|
|
130
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
131
|
+
const drawn = relations.flatMap((relation) => {
|
|
132
|
+
const from = byId.get(relation.from);
|
|
133
|
+
const to = byId.get(relation.to);
|
|
134
|
+
return from && to ? [{ relation, from: place(from), to: place(to) }] : [];
|
|
135
|
+
});
|
|
136
|
+
return (_jsxs("svg", { "data-slot": "diagram", "data-unresolved-relations": relations.length - drawn.length, viewBox: `0 0 ${CANVAS_WIDTH} ${CANVAS_HEIGHT}`, xmlns: "http://www.w3.org/2000/svg", role: decorative ? undefined : 'img', "aria-label": decorative ? undefined : label, "aria-hidden": decorative || undefined, className: cn('h-auto w-full', className), children: [drawn.map(({ relation, from, to }) => {
|
|
137
|
+
// A label placed beside its line rather than on it. The offset is the
|
|
138
|
+
// line's normal, so a vertical relation's label sits beside it and a
|
|
139
|
+
// horizontal one's sits above it, and neither ever sits on the stroke.
|
|
140
|
+
const dx = to.x - from.x;
|
|
141
|
+
const dy = to.y - from.y;
|
|
142
|
+
const length = Math.hypot(dx, dy);
|
|
143
|
+
const lift = length === 0 ? 0 : RELATION_NAME_LIFT / length;
|
|
144
|
+
return (_jsxs("g", { "data-slot": "diagram-relation", "data-relation": `${relation.from}-${relation.to}`, children: [_jsx("path", { d: `M ${from.x} ${from.y} L ${to.x} ${to.y}`, strokeWidth: 1, strokeDasharray: relation.indirect ? '4 4' : undefined, className: "stroke-muted-foreground fill-none" }), _jsx("text", { x: (from.x + to.x) / 2 - dy * lift, y: (from.y + to.y) / 2 + dx * lift, fontSize: RELATION_NAME_SIZE, textAnchor: "middle", className: "fill-muted-foreground font-mono", children: relation.label })] }, `${relation.from}-${relation.to}`));
|
|
145
|
+
}), nodes.map((node) => {
|
|
146
|
+
const at = place(node);
|
|
147
|
+
return (_jsxs("g", { "data-slot": "diagram-node", "data-node": node.id, "data-emphasis": node.emphasis || undefined, children: [_jsx("circle", { cx: at.x, cy: at.y, r: NODE_RADIUS, strokeWidth: node.emphasis ? 2 : 1, className: cn(node.emphasis ? 'fill-accent stroke-brand-ink' : 'fill-card stroke-border') }), _jsx("text", { x: at.x, y: at.y + NODE_NAME_OFFSET, fontSize: NODE_NAME_SIZE, textAnchor: "middle", className: "fill-foreground font-mono", children: node.name })] }, node.id));
|
|
148
|
+
})] }));
|
|
149
|
+
}
|
|
150
|
+
export { Diagram };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One fact: the term that names it and the value that answers it.
|
|
4
|
+
*
|
|
5
|
+
* `label` is required and `value` is required, because a fact with no value is a
|
|
6
|
+
* heading and a fact with no term is a value with nothing to answer. A fact whose
|
|
7
|
+
* value is a destination passes `href` and gets an anchor, so a reader can
|
|
8
|
+
* follow it rather than read it.
|
|
9
|
+
*/
|
|
10
|
+
export type Fact = {
|
|
11
|
+
/**
|
|
12
|
+
* The name of the fact, the term side of the pair. It is a label rather than
|
|
13
|
+
* prose: a column of these is read by scanning the left edge, so a term that
|
|
14
|
+
* wraps to two lines costs the whole column its alignment.
|
|
15
|
+
*/
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
/** The answer. A string, or any node a consumer composes. */
|
|
18
|
+
value: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Makes the value a link to where the fact is kept. Omit it for a fact whose
|
|
21
|
+
* value is the whole answer, such as a count or a version.
|
|
22
|
+
*/
|
|
23
|
+
href?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Opens the destination in a new browsing context, which defaults the link
|
|
26
|
+
* relationship to `noopener noreferrer`. Prism does not decide what counts as
|
|
27
|
+
* external: a cross-origin destination is not by itself a reason to open a tab.
|
|
28
|
+
*/
|
|
29
|
+
newTab?: boolean;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The props a FactList takes.
|
|
33
|
+
*
|
|
34
|
+
* The facts are the content and the content is a prop: the list ships no fact,
|
|
35
|
+
* no sample row and no placeholder. An empty list renders nothing rather than an
|
|
36
|
+
* empty frame, so a caller that has nothing to say does not get a bordered box
|
|
37
|
+
* with a title above it.
|
|
38
|
+
*/
|
|
39
|
+
export type FactListProps = ComponentProps<'dl'> & {
|
|
40
|
+
/**
|
|
41
|
+
* The facts, in the order a reader should meet them. Order is the caller's
|
|
42
|
+
* because it is a claim about importance, and a list that sorted itself would
|
|
43
|
+
* be making that claim on the caller's behalf.
|
|
44
|
+
*/
|
|
45
|
+
facts: readonly Fact[];
|
|
46
|
+
/**
|
|
47
|
+
* A heading above the list, for a set of facts that needs to say what it is.
|
|
48
|
+
* Rendered as a `dt`-level caption rather than as a heading, so it does not
|
|
49
|
+
* compete with the document's own outline.
|
|
50
|
+
*/
|
|
51
|
+
label?: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Layout only, exactly as on every Component. Changing a Prism-owned visual
|
|
54
|
+
* property from here is prohibited.
|
|
55
|
+
*/
|
|
56
|
+
className?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* A short list of named facts, as a definition list.
|
|
60
|
+
*
|
|
61
|
+
* One job: put a term beside its value in a column a reader can scan, and make
|
|
62
|
+
* the pair a real description list so a screen reader announces the term before
|
|
63
|
+
* the value. Every NaniSoft site states facts beside prose: what a plan costs,
|
|
64
|
+
* what a feed carries, which version a document describes, when a collector went
|
|
65
|
+
* live. Each one re-derived a `<dl>` and each one got the alignment and the
|
|
66
|
+
* term-before-value wiring subtly wrong.
|
|
67
|
+
*
|
|
68
|
+
* It is a Component rather than a Block because it has no content of its own to
|
|
69
|
+
* be passed, and because the fact set is data rather than a section: a Block
|
|
70
|
+
* models a region of a page and this models a shape that appears inside one.
|
|
71
|
+
*
|
|
72
|
+
* The term column is a fixed fraction rather than a content width, so a column
|
|
73
|
+
* of facts lines up down its whole length and a long term pushes the value rather
|
|
74
|
+
* than resizing every other row. Values align to the top, because a value that
|
|
75
|
+
* wraps and a value that does not are both common and centring one of them reads
|
|
76
|
+
* as an error.
|
|
77
|
+
*
|
|
78
|
+
* The hairline between rows is `border-border`, so a list of facts reads as a
|
|
79
|
+
* table without becoming one: no header row, because a fact has no column
|
|
80
|
+
* header, and no `Table` semantics, because these are terms and their answers
|
|
81
|
+
* rather than records.
|
|
82
|
+
*
|
|
83
|
+
* A fact with no value renders the term and nothing else, so a caller that is
|
|
84
|
+
* missing an answer shows a gap rather than a blank that reads as a value.
|
|
85
|
+
*
|
|
86
|
+
* It is a server Component: no hook, no context and no client code.
|
|
87
|
+
*/
|
|
88
|
+
declare function FactList({ facts, label, className, ...props }: FactListProps): import("react").JSX.Element | null;
|
|
89
|
+
export { FactList };
|
|
90
|
+
//# sourceMappingURL=fact-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fact-list.d.ts","sourceRoot":"","sources":["../../../src/components/ui/fact-list.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItD;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG;IACjB;;;;OAIG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,6DAA6D;IAC7D,KAAK,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG;IACjD;;;;OAIG;IACH,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,iBAAS,QAAQ,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,aAAa,sCAuCrE;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
/**
|
|
4
|
+
* A short list of named facts, as a definition list.
|
|
5
|
+
*
|
|
6
|
+
* One job: put a term beside its value in a column a reader can scan, and make
|
|
7
|
+
* the pair a real description list so a screen reader announces the term before
|
|
8
|
+
* the value. Every NaniSoft site states facts beside prose: what a plan costs,
|
|
9
|
+
* what a feed carries, which version a document describes, when a collector went
|
|
10
|
+
* live. Each one re-derived a `<dl>` and each one got the alignment and the
|
|
11
|
+
* term-before-value wiring subtly wrong.
|
|
12
|
+
*
|
|
13
|
+
* It is a Component rather than a Block because it has no content of its own to
|
|
14
|
+
* be passed, and because the fact set is data rather than a section: a Block
|
|
15
|
+
* models a region of a page and this models a shape that appears inside one.
|
|
16
|
+
*
|
|
17
|
+
* The term column is a fixed fraction rather than a content width, so a column
|
|
18
|
+
* of facts lines up down its whole length and a long term pushes the value rather
|
|
19
|
+
* than resizing every other row. Values align to the top, because a value that
|
|
20
|
+
* wraps and a value that does not are both common and centring one of them reads
|
|
21
|
+
* as an error.
|
|
22
|
+
*
|
|
23
|
+
* The hairline between rows is `border-border`, so a list of facts reads as a
|
|
24
|
+
* table without becoming one: no header row, because a fact has no column
|
|
25
|
+
* header, and no `Table` semantics, because these are terms and their answers
|
|
26
|
+
* rather than records.
|
|
27
|
+
*
|
|
28
|
+
* A fact with no value renders the term and nothing else, so a caller that is
|
|
29
|
+
* missing an answer shows a gap rather than a blank that reads as a value.
|
|
30
|
+
*
|
|
31
|
+
* It is a server Component: no hook, no context and no client code.
|
|
32
|
+
*/
|
|
33
|
+
function FactList({ facts, label, className, ...props }) {
|
|
34
|
+
if (facts.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
return (_jsxs("dl", { "data-slot": "fact-list", className: cn('flex w-full flex-col text-sm', className), ...props, children: [label ? (_jsx("dt", { className: "text-muted-foreground mb-2 text-xs font-medium tracking-wide uppercase", children: label })) : null, facts.map((fact, index) => (_jsxs("div", { "data-slot": "fact-list-row", className: "border-border flex items-start justify-between gap-6 border-b py-2 last:border-b-0", children: [_jsx("dt", { className: "text-muted-foreground shrink-0 font-medium", children: fact.label }), _jsx("dd", { className: "text-right text-balance", children: fact.href ? (_jsx("a", { href: fact.href, rel: fact.newTab ? 'noopener noreferrer' : undefined, target: fact.newTab ? '_blank' : undefined, className: "underline underline-offset-4 hover:underline", children: fact.value })) : (fact.value) })] }, index)))] }));
|
|
37
|
+
}
|
|
38
|
+
export { FactList };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { PackId } from '../../theming';
|
|
2
|
+
/**
|
|
3
|
+
* The three sizes a product mark is drawn at: the mark's diameter and the name
|
|
4
|
+
* beside it, taken together so the two parts of the mark stay in proportion.
|
|
5
|
+
*
|
|
6
|
+
* The mark is one of a fixed set rather than something a caller sizes through
|
|
7
|
+
* `className`, because `className` is layout only and a mark sized by a layout
|
|
8
|
+
* class is a mark the layout owns. A product set shows its marks in a switcher,
|
|
9
|
+
* in a row and on a page header, and those three are the sizes a set needs.
|
|
10
|
+
*/
|
|
11
|
+
declare const SIZES: {
|
|
12
|
+
readonly sm: {
|
|
13
|
+
readonly mark: "size-4";
|
|
14
|
+
readonly name: "text-xs";
|
|
15
|
+
};
|
|
16
|
+
readonly md: {
|
|
17
|
+
readonly mark: "size-5";
|
|
18
|
+
readonly name: "text-sm";
|
|
19
|
+
};
|
|
20
|
+
readonly lg: {
|
|
21
|
+
readonly mark: "size-6";
|
|
22
|
+
readonly name: "text-base";
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The three sizes a product mark is drawn at.
|
|
27
|
+
*
|
|
28
|
+
* @defaultValue 'md'
|
|
29
|
+
*/
|
|
30
|
+
export type ProductMarkSize = keyof typeof SIZES;
|
|
31
|
+
/**
|
|
32
|
+
* The props a ProductMark takes.
|
|
33
|
+
*
|
|
34
|
+
* There is no product directory in this package. A product's identity is the
|
|
35
|
+
* consumer's data, so the Component takes the three facts the mark is drawn
|
|
36
|
+
* from and owns none of them: `id` for the markup, `name` for the second part,
|
|
37
|
+
* and `pack` for the hue. Where the set of products is published is the
|
|
38
|
+
* consumer's decision, and Prism ships the mark rather than a list of products.
|
|
39
|
+
*/
|
|
40
|
+
export type ProductMarkProps = {
|
|
41
|
+
/**
|
|
42
|
+
* The product's key within the set it is distinguished in. It is carried on
|
|
43
|
+
* the markup as `data-product` so a test can name one mark rather than the
|
|
44
|
+
* first one, and so two marks in one document are individually addressable.
|
|
45
|
+
*/
|
|
46
|
+
id: string;
|
|
47
|
+
/**
|
|
48
|
+
* The product's name, which is the mark's second part and the word that
|
|
49
|
+
* carries the identity. The mark sets it in `brand-ink` rather than in
|
|
50
|
+
* `foreground`, which is DESIGN.md's Brand Ink Rule: a name that has to read
|
|
51
|
+
* as the brand reads `brand-ink`, and `foreground` is a tinted neutral with
|
|
52
|
+
* almost no chroma by design.
|
|
53
|
+
*/
|
|
54
|
+
name: string;
|
|
55
|
+
/**
|
|
56
|
+
* The product's pack, which is the mark's hue. Omit it, or pass `null`, for
|
|
57
|
+
* the one entry in a set that has no pack of its own: that entry is drawn as
|
|
58
|
+
* the full spectrum rather than as a colourless mark. See `SPECTRUM` above.
|
|
59
|
+
*
|
|
60
|
+
* `default` is a real answer and not the same as the empty one. The base pack
|
|
61
|
+
* is a pack a product can wear, and it is the absence of a `data-pack`
|
|
62
|
+
* attribute, so a product whose pack is `default` carries no attribute and its
|
|
63
|
+
* core resolves the base pack's `primary` from the page.
|
|
64
|
+
*/
|
|
65
|
+
pack?: PackId | null;
|
|
66
|
+
/**
|
|
67
|
+
* Which of the three drawn sizes to use. @defaultValue 'md'
|
|
68
|
+
*/
|
|
69
|
+
size?: ProductMarkSize;
|
|
70
|
+
/**
|
|
71
|
+
* Layout only, exactly as on every Component. Changing a Prism-owned visual
|
|
72
|
+
* property from here is prohibited, and this Component spreads no other prop,
|
|
73
|
+
* so there is no second route to its ink.
|
|
74
|
+
*/
|
|
75
|
+
className?: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* A product's two-part colour mark: a hairline ring in the brand ink around a
|
|
79
|
+
* core in the pack's own fill, beside the product's name in the brand ink.
|
|
80
|
+
*
|
|
81
|
+
* Two parts, and both colours come from tokens, which is the whole reason this
|
|
82
|
+
* is promoted out of the two hand-written copies it replaces. The ring is
|
|
83
|
+
* `brand-ink`, the pack's brand hue as a stroke; the core is `primary`, the
|
|
84
|
+
* pack's brand value as a fill. That split is not a preference. DESIGN.md's Fill,
|
|
85
|
+
* Not the Ink Rule says a pastel brand value is a fill and never text, so the
|
|
86
|
+
* core may be `primary` and the name may not, and the Brand Ink Rule says the
|
|
87
|
+
* name reads `brand-ink` rather than `foreground` or `primary-foreground`. A
|
|
88
|
+
* hand-written copy gets one of those two wrong and nothing in the rendered
|
|
89
|
+
* result says which.
|
|
90
|
+
*
|
|
91
|
+
* Uniqueness across the set comes from the pack: each product wears its own
|
|
92
|
+
* pack's hue, and the one entry with no pack wears the spectrum, so no two marks
|
|
93
|
+
* in a set resolve to the same pair of colours. The set is the consumer's data.
|
|
94
|
+
* This Component draws one member of it, and it is the same member wherever it is
|
|
95
|
+
* drawn from.
|
|
96
|
+
*
|
|
97
|
+
* The mark carries its own `data-pack` boundary on its root, so a product's hue
|
|
98
|
+
* follows from the pack the caller passed rather than from whichever pack the
|
|
99
|
+
* page happens to be wearing, and a boundary lands on the mark rather than on
|
|
100
|
+
* whatever encloses it. The boundary is on a span carrying no radius utility,
|
|
101
|
+
* which is one of the two placements DESIGN.md's pack-boundary law allows, and
|
|
102
|
+
* the disc itself is fully rounded. Nothing about the mark's shape moves when a
|
|
103
|
+
* pack boundary lands above it, which is the half of that law a consumer
|
|
104
|
+
* implementing only the colour half gets wrong.
|
|
105
|
+
*
|
|
106
|
+
* It is a server Component: no hook, no mode, no context, no client code and no
|
|
107
|
+
* `'use client'` line. A consumer renders it from a server file and needs
|
|
108
|
+
* nothing from the provider mounted.
|
|
109
|
+
*
|
|
110
|
+
* The disc is `aria-hidden` and the name is not, so the mark adds a shape beside
|
|
111
|
+
* the word rather than a second reading of it.
|
|
112
|
+
*/
|
|
113
|
+
declare function ProductMark({ id, name, pack, size, className }: ProductMarkProps): import("react").JSX.Element;
|
|
114
|
+
export { ProductMark };
|
|
115
|
+
//# sourceMappingURL=product-mark.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-mark.d.ts","sourceRoot":"","sources":["../../../src/components/ui/product-mark.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AA2B3C;;;;;;;;GAQG;AACH,QAAA,MAAM,KAAK;;;;;;;;;;;;;CAID,CAAA;AAEV;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,KAAK,CAAA;AAEhD;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,iBAAS,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAW,EAAE,SAAS,EAAE,EAAE,gBAAgB,+BA2BhF;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
|