@shabboo/kit 0.0.0-stage → 0.1.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.
@@ -0,0 +1,124 @@
1
+ ---
2
+ /**
3
+ * Previous, numbered pages and next. Pass the pages you want shown (with `current` on one);
4
+ * a missing prev/next leaves an empty, hidden spacer in its place so the row doesn't jump.
5
+ */
6
+ import type { HTMLAttributes } from "astro/types";
7
+ import Icon from "./Icon.astro";
8
+
9
+ interface PageLink {
10
+ href: string;
11
+ label: string;
12
+ }
13
+
14
+ interface Props extends HTMLAttributes<"nav"> {
15
+ prev?: PageLink;
16
+ next?: PageLink;
17
+ pages?: (PageLink & { current?: boolean })[];
18
+ /** accessible name of the nav landmark */
19
+ label?: string;
20
+ }
21
+
22
+ const { prev, next, pages = [], label = "Pagination", class: className, ...rest } = Astro.props;
23
+ ---
24
+
25
+ <nav aria-label={label} class:list={["pager", className]} {...rest}>
26
+ {
27
+ prev ? (
28
+ <a class="end" href={prev.href} rel="prev">
29
+ <Icon name="arrow-left" size="sm" />
30
+ {prev.label}
31
+ </a>
32
+ ) : (
33
+ <span class="end off" aria-hidden="true" />
34
+ )
35
+ }
36
+ {
37
+ pages.length > 0 && (
38
+ <ol>
39
+ {pages.map((p) => (
40
+ <li>
41
+ <a href={p.href} aria-current={p.current ? "page" : undefined}>
42
+ {p.label}
43
+ </a>
44
+ </li>
45
+ ))}
46
+ </ol>
47
+ )
48
+ }
49
+ {
50
+ next ? (
51
+ <a class="end" href={next.href} rel="next">
52
+ {next.label}
53
+ <Icon name="arrow-right" size="sm" />
54
+ </a>
55
+ ) : (
56
+ <span class="end off" aria-hidden="true" />
57
+ )
58
+ }
59
+ </nav>
60
+
61
+ <style>
62
+ .pager {
63
+ display: flex;
64
+ flex-wrap: wrap;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ gap: var(--space-4);
68
+ font-family: var(--font-mono);
69
+ font-size: var(--step--1);
70
+ }
71
+ .pager:is(:lang(fa), :lang(ar)) {
72
+ font-family: var(--font-arabic);
73
+ }
74
+ a {
75
+ text-decoration: none;
76
+ }
77
+ .end {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: var(--space-2);
81
+ min-inline-size: 6rem;
82
+ }
83
+ .end:last-child {
84
+ justify-content: flex-end;
85
+ }
86
+ ol {
87
+ display: flex;
88
+ gap: var(--space-1);
89
+ list-style: none;
90
+ padding: 0;
91
+ }
92
+ ol a {
93
+ display: grid;
94
+ place-items: center;
95
+ min-inline-size: var(--control-h-sm);
96
+ min-block-size: var(--control-h-sm);
97
+ padding-inline: var(--space-1);
98
+ border: 1px solid transparent;
99
+ border-radius: var(--radius);
100
+ color: var(--text-muted);
101
+ }
102
+ ol a:hover {
103
+ border-color: var(--stroke);
104
+ background: var(--state-hover);
105
+ }
106
+ ol a:active {
107
+ background: var(--state-active);
108
+ }
109
+ ol a[aria-current="page"] {
110
+ border-color: var(--stroke-strong);
111
+ background: var(--state-selected);
112
+ color: var(--text-accent);
113
+ }
114
+ /* on touch, each page is a full --target-min square: the numbers sit too close to grow only the hit area */
115
+ @media (pointer: coarse) {
116
+ ol a {
117
+ min-inline-size: var(--target-min);
118
+ min-block-size: var(--target-min);
119
+ }
120
+ .end {
121
+ min-block-size: var(--target-min);
122
+ }
123
+ }
124
+ </style>
@@ -0,0 +1,121 @@
1
+ ---
2
+ /**
3
+ * The pixel style without sprites: a stepped border with notched corners, drawn with box-shadow
4
+ * steps around a panel (the HUD look). slot "title": a tab on the top edge, e.g. "ch 03".
5
+ * px: the size of one pixel step.
6
+ */
7
+ import type { HTMLAttributes } from "astro/types";
8
+
9
+ interface Props extends HTMLAttributes<"div"> {
10
+ tone?: "accent" | "muted" | "signal";
11
+ size?: "sm" | "md" | "lg";
12
+ title?: string;
13
+ /** CRT scanlines rolling over the panel */
14
+ scanlines?: boolean;
15
+ }
16
+
17
+ const { tone = "accent", size = "md", title, scanlines = false, class: className, ...rest } = Astro.props;
18
+ const hasTitle = Boolean(title) || Astro.slots.has("title");
19
+ ---
20
+
21
+ <div class:list={["pixel", className, { scanlines }]} data-tone={tone} data-size={size} {...rest}>
22
+ {
23
+ hasTitle && (
24
+ <p class="pixel-title">
25
+ <slot name="title">{title}</slot>
26
+ </p>
27
+ )
28
+ }
29
+ <slot />
30
+ </div>
31
+
32
+ <style>
33
+ .pixel {
34
+ --px: 4px;
35
+ --pixel-color: var(--fill-accent-strong);
36
+ /* the title tab: a fill and the text that reads on it at 4.5:1 or more, set together per tone */
37
+ --pixel-tab: var(--fill-accent);
38
+ --pixel-tab-text: var(--text-on-fill);
39
+ position: relative;
40
+ margin: var(--px);
41
+ padding: var(--space-4) var(--space-5);
42
+ background: var(--surface-raised);
43
+ /* four offset copies of the edge leave the corners notched: the classic 8-bit frame */
44
+ box-shadow:
45
+ 0 calc(-1 * var(--px)) 0 0 var(--pixel-color),
46
+ 0 var(--px) 0 0 var(--pixel-color),
47
+ calc(-1 * var(--px)) 0 0 0 var(--pixel-color),
48
+ var(--px) 0 0 0 var(--pixel-color),
49
+ 0 calc(2 * var(--px)) 0 0 var(--surface-sunken);
50
+ image-rendering: pixelated;
51
+ }
52
+ .pixel[data-size="sm"] {
53
+ --px: 2px;
54
+ padding: var(--space-2) var(--space-3);
55
+ font-size: var(--step--1);
56
+ }
57
+ .pixel[data-size="lg"] {
58
+ --px: 6px;
59
+ padding: var(--space-5) var(--space-6);
60
+ }
61
+ .pixel[data-tone="muted"] {
62
+ --pixel-color: var(--stroke);
63
+ --pixel-tab: var(--stroke);
64
+ --pixel-tab-text: var(--text);
65
+ }
66
+ .pixel[data-tone="signal"] {
67
+ --pixel-color: var(--text-signal);
68
+ --pixel-tab: var(--text-signal);
69
+ --pixel-tab-text: var(--surface);
70
+ }
71
+ .pixel-title {
72
+ position: absolute;
73
+ inset-block-start: calc(-1 * var(--px) - 0.9em);
74
+ inset-inline-start: var(--space-4);
75
+ padding: 0.15em 0.6em;
76
+ background: var(--pixel-tab);
77
+ color: var(--pixel-tab-text);
78
+ font-family: var(--font-mono);
79
+ font-size: var(--step--2);
80
+ letter-spacing: var(--tracking-mono);
81
+ text-transform: uppercase;
82
+ line-height: 1.4;
83
+ }
84
+ .pixel-title:is(:lang(fa), :lang(ar)) {
85
+ font-family: var(--font-arabic);
86
+ letter-spacing: 0;
87
+ }
88
+ .scanlines::after {
89
+ content: "";
90
+ position: absolute;
91
+ inset: 0;
92
+ pointer-events: none;
93
+ background: repeating-linear-gradient(to bottom, transparent 0 2px, var(--surface-sunken) 2px 3px);
94
+ opacity: 0.45;
95
+ mix-blend-mode: multiply;
96
+ }
97
+ /* both run under five seconds and then hold (WCAG 2.2.2); the roll is seamless, so it stops where it began */
98
+ @media (prefers-reduced-motion: no-preference) {
99
+ .pixel[data-tone="signal"] .pixel-title {
100
+ animation: pixel-rec var(--blink) steps(1, end) 4;
101
+ }
102
+ .scanlines::after {
103
+ animation: pixel-roll calc(var(--blink) * 4) steps(12, end);
104
+ }
105
+ }
106
+ @keyframes pixel-rec {
107
+ 50% {
108
+ background: transparent;
109
+ color: var(--text-signal);
110
+ }
111
+ }
112
+ @keyframes pixel-roll {
113
+ to {
114
+ background-position: 0 12px;
115
+ }
116
+ }
117
+ .pixel:has(.pixel-title) {
118
+ margin-block-start: calc(var(--px) + 1em);
119
+ padding-block-start: var(--space-5);
120
+ }
121
+ </style>
@@ -0,0 +1,49 @@
1
+ ---
2
+ /** A quotation with its source: <figure><blockquote> + an optional cite line. */
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"figure"> {
6
+ /** who or what said it */
7
+ cite?: string;
8
+ /** link to the source */
9
+ citeUrl?: string;
10
+ size?: "md" | "lg";
11
+ }
12
+
13
+ const { cite, citeUrl, size = "md", class: className, ...rest } = Astro.props;
14
+ ---
15
+
16
+ <figure class:list={["quote", className]} data-size={size} {...rest}>
17
+ <blockquote cite={citeUrl}><slot /></blockquote>
18
+ {
19
+ cite && (
20
+ <figcaption class="label">
21
+ — {citeUrl ? <a href={citeUrl}><cite>{cite}</cite></a> : <cite>{cite}</cite>}
22
+ </figcaption>
23
+ )
24
+ }
25
+ </figure>
26
+
27
+ <style>
28
+ .quote {
29
+ display: grid;
30
+ gap: var(--space-3);
31
+ padding-inline-start: var(--space-5);
32
+ border-inline-start: 2px solid var(--fill-accent-strong);
33
+ }
34
+ blockquote {
35
+ color: var(--text-muted);
36
+ font-style: italic;
37
+ }
38
+ .quote[data-size="lg"] blockquote {
39
+ color: var(--text);
40
+ font-size: var(--step-2);
41
+ line-height: 1.35;
42
+ }
43
+ blockquote:is(:lang(fa), :lang(ar)) {
44
+ font-style: normal;
45
+ }
46
+ cite {
47
+ font-style: normal;
48
+ }
49
+ </style>
@@ -0,0 +1,91 @@
1
+ ---
2
+ /**
3
+ * A small tag. variant "chip": a bordered mono chip (project stacks) · "hash": an accent #tag link (post tags).
4
+ * Renders a link when `href` is set; `as="li"` lets a TagList item be the chip itself.
5
+ */
6
+ import type { HTMLAttributes } from "astro/types";
7
+
8
+ interface Props extends HTMLAttributes<"a"> {
9
+ variant?: "chip" | "hash";
10
+ size?: "sm" | "md";
11
+ as?: "span" | "li";
12
+ }
13
+
14
+ const { variant = "chip", size = "sm", as = "span", href, class: className, ...rest } = Astro.props;
15
+ const Tag = href ? "a" : as;
16
+ ---
17
+
18
+ <Tag class:list={["tag", className]} data-variant={variant} data-size={size} href={href} {...rest}
19
+ >{variant === "hash" && <span class="hash" aria-hidden="true">#</span>}<slot /></Tag
20
+ >
21
+
22
+ <style>
23
+ .tag {
24
+ font-family: var(--font-mono);
25
+ font-size: var(--step--2);
26
+ }
27
+ /* mono has no Arabic script: use the Arabic face */
28
+ .tag:is(:lang(fa), :lang(ar)) {
29
+ font-family: var(--font-arabic);
30
+ }
31
+ .tag[data-size="md"] {
32
+ font-size: var(--step--1);
33
+ }
34
+ .tag[data-variant="chip"] {
35
+ padding: 0.1em 0.5em;
36
+ border: 1px solid var(--stroke);
37
+ border-radius: var(--radius);
38
+ }
39
+ /* a chip you can press draws its edge as a control (3:1); a plain chip keeps the hairline */
40
+ a.tag[data-variant="chip"] {
41
+ border-color: var(--stroke-control);
42
+ text-decoration: none;
43
+ }
44
+ /* drawn small, tapped big: a linked chip's hit area reaches --target-min in height (WCAG 2.5.5) */
45
+ a.tag {
46
+ position: relative;
47
+ }
48
+ a.tag::after {
49
+ content: "";
50
+ position: absolute;
51
+ inset-inline: 0;
52
+ inset-block: min(0px, (100% - var(--target-min)) / 2);
53
+ }
54
+ a.tag[data-variant="chip"]:hover {
55
+ border-color: var(--stroke-strong);
56
+ background: var(--state-hover);
57
+ }
58
+ a.tag[data-variant="chip"]:active {
59
+ background: var(--state-active);
60
+ }
61
+ a.tag[data-variant="hash"]:hover {
62
+ color: var(--text);
63
+ }
64
+ @media (prefers-reduced-motion: no-preference) {
65
+ a.tag {
66
+ transition:
67
+ color var(--dur-step) var(--ease-step),
68
+ border-color var(--dur-step) var(--ease-step),
69
+ background-color var(--dur-step) var(--ease-step);
70
+ }
71
+ a.tag[data-variant="hash"]:hover .hash {
72
+ display: inline-block;
73
+ animation: tag-glitch calc(var(--dur-step) * 2) steps(1, end);
74
+ }
75
+ }
76
+ @keyframes tag-glitch {
77
+ 0% {
78
+ translate: 0 -2px;
79
+ }
80
+ 33% {
81
+ translate: 2px 0;
82
+ }
83
+ 66% {
84
+ translate: -1px 1px;
85
+ }
86
+ }
87
+ .tag[data-variant="hash"] {
88
+ color: var(--text-accent);
89
+ text-decoration: none;
90
+ }
91
+ </style>
@@ -0,0 +1,68 @@
1
+ ---
2
+ /**
3
+ * A wrapping list of Tags. Microformat classes go where each app already puts them:
4
+ * `itemClass` on the <li>, `linkClass` on the link (e.g. "p-category").
5
+ */
6
+ import type { HTMLAttributes } from "astro/types";
7
+ import Tag from "./Tag.astro";
8
+
9
+ export interface TagItem {
10
+ label: string;
11
+ href?: string;
12
+ }
13
+
14
+ interface Props extends HTMLAttributes<"ul"> {
15
+ items: TagItem[];
16
+ variant?: "chip" | "hash";
17
+ size?: "sm" | "md";
18
+ /** accessible name of the list, e.g. "Tags" or "Stack" */
19
+ label: string;
20
+ itemClass?: string;
21
+ linkClass?: string;
22
+ /** rel on each link, e.g. "tag" */
23
+ rel?: string;
24
+ }
25
+
26
+ const {
27
+ items,
28
+ variant = "chip",
29
+ size = "sm",
30
+ label,
31
+ itemClass,
32
+ linkClass,
33
+ rel,
34
+ class: className,
35
+ ...rest
36
+ } = Astro.props;
37
+ ---
38
+
39
+ <ul class:list={["tag-list", className]} data-variant={variant} aria-label={label} {...rest}>
40
+ {
41
+ items.map((item) =>
42
+ item.href ? (
43
+ <li class={itemClass}>
44
+ <Tag variant={variant} size={size} href={item.href} class={linkClass} rel={rel}>
45
+ {item.label}
46
+ </Tag>
47
+ </li>
48
+ ) : (
49
+ <Tag as="li" variant={variant} size={size} class={itemClass}>
50
+ {item.label}
51
+ </Tag>
52
+ ),
53
+ )
54
+ }
55
+ </ul>
56
+
57
+ <style>
58
+ .tag-list {
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ gap: var(--space-2);
62
+ list-style: none;
63
+ padding: 0;
64
+ }
65
+ .tag-list[data-variant="hash"] {
66
+ gap: var(--space-3);
67
+ }
68
+ </style>
@@ -0,0 +1,23 @@
1
+ ---
2
+ /** A vertical dated list; fill it with <TimelineItem>s, newest first. */
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"ol"> {
6
+ label?: string;
7
+ }
8
+
9
+ const { label, class: className, ...rest } = Astro.props;
10
+ ---
11
+
12
+ <ol class:list={["timeline", className]} aria-label={label} {...rest}><slot /></ol>
13
+
14
+ <style>
15
+ .timeline {
16
+ display: grid;
17
+ gap: var(--space-6);
18
+ list-style: none;
19
+ padding: 0;
20
+ padding-inline-start: var(--space-5);
21
+ border-inline-start: 1px dashed var(--stroke);
22
+ }
23
+ </style>
@@ -0,0 +1,105 @@
1
+ ---
2
+ /**
3
+ * One step on a Timeline: a dated range, a title and an optional subtitle; body in the default slot.
4
+ * Marked up as an h-event (dt-start / dt-end). The end is for parsers only: `dates` already says it aloud.
5
+ * `href` links the title (e.g. an institution); slot "subtitle" takes markup the string prop can't (e.g. a p-location).
6
+ */
7
+ import type { HTMLAttributes } from "astro/types";
8
+
9
+ interface Props extends HTMLAttributes<"li"> {
10
+ title: string;
11
+ /** links the title */
12
+ href?: string;
13
+ subtitle?: string;
14
+ /** machine-readable start, e.g. "2021-03"; omit when only the end is known (the dates still show) */
15
+ start?: string;
16
+ /** machine-readable end; omit for "present" */
17
+ end?: string;
18
+ /** the dates as shown, e.g. "2021 — now" */
19
+ dates: string;
20
+ current?: boolean;
21
+ /** the title's heading level, so it fits the page outline (the Timeline usually sits under an h2) */
22
+ headingLevel?: 2 | 3 | 4;
23
+ }
24
+
25
+ const {
26
+ title,
27
+ href,
28
+ subtitle,
29
+ start,
30
+ end,
31
+ dates,
32
+ current = false,
33
+ headingLevel = 3,
34
+ class: className,
35
+ ...rest
36
+ } = Astro.props;
37
+ const Heading = `h${headingLevel}` as const;
38
+ ---
39
+
40
+ <li class:list={["step h-event", className, { current }]} {...rest}>
41
+ <p class="label">
42
+ {start ? <time class="dt-start" datetime={start}>{dates}</time> : <span>{dates}</span>}
43
+ {end && <time class="dt-end" datetime={end} hidden />}
44
+ </p>
45
+ <Heading class="p-name">{href ? <a class="u-url" href={href}>{title}</a> : title}</Heading>
46
+ {
47
+ Astro.slots.has("subtitle") ? (
48
+ <p class="step-sub"><slot name="subtitle" /></p>
49
+ ) : (
50
+ subtitle && <p class="step-sub">{subtitle}</p>
51
+ )
52
+ }
53
+ {Astro.slots.has("default") && <div class="p-description step-body"><slot /></div>}
54
+ </li>
55
+
56
+ <style>
57
+ .step {
58
+ position: relative;
59
+ display: grid;
60
+ gap: var(--space-1);
61
+ }
62
+ /* the dot on the rail */
63
+ .step::before {
64
+ content: "";
65
+ position: absolute;
66
+ inset-inline-start: calc(-1 * var(--space-5) - 4px);
67
+ inset-block-start: 0.45em;
68
+ inline-size: 7px;
69
+ block-size: 7px;
70
+ border: 1px solid var(--fill-accent-strong);
71
+ border-radius: 50%;
72
+ background: var(--surface);
73
+ }
74
+ .current::before {
75
+ background: var(--text-accent);
76
+ box-shadow: 0 0 0 4px var(--fill-glow);
77
+ }
78
+ /* three pulses, under five seconds, then it holds (WCAG 2.2.2) */
79
+ @media (prefers-reduced-motion: no-preference) {
80
+ .current::before {
81
+ animation: step-blink calc(var(--blink) * 1.5) steps(1, end) 3;
82
+ }
83
+ }
84
+ @keyframes step-blink {
85
+ 50% {
86
+ box-shadow: 0 0 0 2px var(--fill-glow);
87
+ }
88
+ }
89
+ .p-name {
90
+ font-size: var(--step-1);
91
+ }
92
+ .step-sub {
93
+ color: var(--text-accent);
94
+ font-style: italic;
95
+ }
96
+ .step-body {
97
+ color: var(--text-muted);
98
+ margin-block-start: var(--space-2);
99
+ }
100
+ /* Persian and Arabic script is never slanted */
101
+ :is(:lang(fa), :lang(ar)) :where(*),
102
+ :is(:lang(fa), :lang(ar)) {
103
+ font-style: normal;
104
+ }
105
+ </style>
@@ -0,0 +1,27 @@
1
+ /** Paths for <Icon>: a 24px grid, 1.5 stroke, round caps. Add an icon by adding a path. */
2
+ export const PATHS = {
3
+ "arrow-right": "M4 12h15M13 6l6 6-6 6",
4
+ "arrow-left": "M20 12H5M11 6l-6 6 6 6",
5
+ "arrow-down": "M12 4v15M6 13l6 6 6-6",
6
+ "arrow-up": "M12 20V5M6 11l6-6 6 6",
7
+ download: "M12 4v11M7 10l5 5 5-5M5 20h14",
8
+ external: "M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5",
9
+ "chevron-down": "M6 9l6 6 6-6",
10
+ "chevron-right": "M9 6l6 6-6 6",
11
+ close: "M6 6l12 12M18 6L6 18",
12
+ check: "M5 12.5l4.5 4.5L19 7",
13
+ info: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 11v6M12 7.5v.01",
14
+ warning: "M12 3.5l9.5 16.5h-19L12 3.5zM12 10v4.5M12 17.5v.01",
15
+ search: "M10.5 18a7.5 7.5 0 1 0 0-15 7.5 7.5 0 0 0 0 15zM16 16l5 5",
16
+ copy: "M9 9h11v11H9zM5 15H4V4h11v1",
17
+ rss: "M5 5a14 14 0 0 1 14 14M5 11a8 8 0 0 1 8 8M6 19.01V19",
18
+ mail: "M3 6h18v12H3zM3 6l9 7 9-7",
19
+ github:
20
+ "M9 19c-4.5 1.4-4.5-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2 4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12 12 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21",
21
+ moon: "M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z",
22
+ flower:
23
+ "M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM12 9V4M12 15v5M9 12H4M15 12h5M9.9 9.9 6.3 6.3M14.1 14.1l3.6 3.6M14.1 9.9l3.6-3.6M9.9 14.1l-3.6 3.6",
24
+ } as const;
25
+
26
+ export type IconName = keyof typeof PATHS;
27
+ export const ICON_NAMES = Object.keys(PATHS) as IconName[];