@relevate/katachi 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.
Files changed (92) hide show
  1. package/CONTRIBUTING.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -0
  4. package/bin/katachi.mjs +30 -0
  5. package/dist/api/index.d.ts +54 -0
  6. package/dist/api/index.js +45 -0
  7. package/dist/api/jsx.d.ts +26 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +77 -0
  10. package/dist/core/ast.d.ts +115 -0
  11. package/dist/core/ast.js +51 -0
  12. package/dist/core/build.d.ts +15 -0
  13. package/dist/core/build.js +107 -0
  14. package/dist/core/compiler.d.ts +9 -0
  15. package/dist/core/compiler.js +9 -0
  16. package/dist/core/example-fixtures.d.ts +5 -0
  17. package/dist/core/example-fixtures.js +54 -0
  18. package/dist/core/parser.d.ts +5 -0
  19. package/dist/core/parser.js +637 -0
  20. package/dist/core/types.d.ts +65 -0
  21. package/dist/core/types.js +1 -0
  22. package/dist/core/verify.d.ts +25 -0
  23. package/dist/core/verify.js +270 -0
  24. package/dist/index.d.ts +7 -0
  25. package/dist/index.js +5 -0
  26. package/dist/targets/askama.d.ts +11 -0
  27. package/dist/targets/askama.js +122 -0
  28. package/dist/targets/index.d.ts +5 -0
  29. package/dist/targets/index.js +60 -0
  30. package/dist/targets/react.d.ts +4 -0
  31. package/dist/targets/react.js +28 -0
  32. package/dist/targets/shared.d.ts +36 -0
  33. package/dist/targets/shared.js +278 -0
  34. package/dist/targets/static-jsx.d.ts +4 -0
  35. package/dist/targets/static-jsx.js +28 -0
  36. package/dist/verify-examples.d.ts +1 -0
  37. package/dist/verify-examples.js +14 -0
  38. package/docs/architecture.md +122 -0
  39. package/docs/getting-started.md +154 -0
  40. package/docs/syntax.md +236 -0
  41. package/docs/targets.md +53 -0
  42. package/examples/basic/README.md +67 -0
  43. package/examples/basic/components/badge-chip.html +3 -0
  44. package/examples/basic/components/comparison-table.html +24 -0
  45. package/examples/basic/components/glyph.html +6 -0
  46. package/examples/basic/components/hover-note.html +6 -0
  47. package/examples/basic/components/media-frame.html +15 -0
  48. package/examples/basic/components/notice-panel.html +24 -0
  49. package/examples/basic/components/resource-tile.html +24 -0
  50. package/examples/basic/components/stack-shell.html +3 -0
  51. package/examples/basic/dist/askama/badge-chip.rs +18 -0
  52. package/examples/basic/dist/askama/comparison-table.rs +47 -0
  53. package/examples/basic/dist/askama/glyph.rs +21 -0
  54. package/examples/basic/dist/askama/hover-note.rs +19 -0
  55. package/examples/basic/dist/askama/includes/badge-chip.html +5 -0
  56. package/examples/basic/dist/askama/includes/comparison-table.html +34 -0
  57. package/examples/basic/dist/askama/includes/glyph.html +6 -0
  58. package/examples/basic/dist/askama/includes/hover-note.html +6 -0
  59. package/examples/basic/dist/askama/includes/media-frame.html +23 -0
  60. package/examples/basic/dist/askama/includes/notice-panel.html +34 -0
  61. package/examples/basic/dist/askama/includes/resource-tile.html +42 -0
  62. package/examples/basic/dist/askama/includes/stack-shell.html +5 -0
  63. package/examples/basic/dist/askama/media-frame.rs +37 -0
  64. package/examples/basic/dist/askama/notice-panel.rs +49 -0
  65. package/examples/basic/dist/askama/resource-tile.rs +59 -0
  66. package/examples/basic/dist/askama/stack-shell.rs +17 -0
  67. package/examples/basic/dist/jsx-static/badge-chip.tsx +18 -0
  68. package/examples/basic/dist/jsx-static/comparison-table.tsx +53 -0
  69. package/examples/basic/dist/jsx-static/glyph.tsx +21 -0
  70. package/examples/basic/dist/jsx-static/hover-note.tsx +19 -0
  71. package/examples/basic/dist/jsx-static/media-frame.tsx +41 -0
  72. package/examples/basic/dist/jsx-static/notice-panel.tsx +53 -0
  73. package/examples/basic/dist/jsx-static/resource-tile.tsx +63 -0
  74. package/examples/basic/dist/jsx-static/stack-shell.tsx +17 -0
  75. package/examples/basic/dist/react/badge-chip.tsx +18 -0
  76. package/examples/basic/dist/react/comparison-table.tsx +53 -0
  77. package/examples/basic/dist/react/glyph.tsx +21 -0
  78. package/examples/basic/dist/react/hover-note.tsx +19 -0
  79. package/examples/basic/dist/react/media-frame.tsx +41 -0
  80. package/examples/basic/dist/react/notice-panel.tsx +53 -0
  81. package/examples/basic/dist/react/resource-tile.tsx +63 -0
  82. package/examples/basic/dist/react/stack-shell.tsx +17 -0
  83. package/examples/basic/src/templates/badge-chip.template.tsx +18 -0
  84. package/examples/basic/src/templates/comparison-table.template.tsx +35 -0
  85. package/examples/basic/src/templates/glyph.template.tsx +17 -0
  86. package/examples/basic/src/templates/hover-note.template.tsx +17 -0
  87. package/examples/basic/src/templates/media-frame.template.tsx +25 -0
  88. package/examples/basic/src/templates/notice-panel.template.tsx +40 -0
  89. package/examples/basic/src/templates/resource-tile.template.tsx +51 -0
  90. package/examples/basic/src/templates/stack-shell.template.tsx +13 -0
  91. package/examples/basic/tsconfig.json +10 -0
  92. package/package.json +69 -0
@@ -0,0 +1,34 @@
1
+ <aside
2
+ class='rounded-3xl border px-5 py-4 backdrop-blur-sm {% if tone == "calm" %}border-sky-200 bg-sky-50/80{% endif %} {% if tone == "urgent" %}border-rose-200 bg-rose-50/80{% endif %} {% if tone == "success" %}border-emerald-200 bg-emerald-50/80{% endif %}'
3
+ >
4
+ <div
5
+ class='flex items-start gap-3'
6
+ >
7
+ {% let className = "mt-0.5 h-5 w-5 shrink-0" %}
8
+ {% let tone = tone %}
9
+ {% let size = "18" %}
10
+ {% let name = icon %}
11
+ {% include "./includes/glyph.html" %}
12
+ <div
13
+ class='min-w-0 flex-1'
14
+ >
15
+ <h3
16
+ class='text-sm font-semibold tracking-tight'
17
+ >
18
+ {{ title }}
19
+ </h3>
20
+ <div
21
+ class='mt-2 text-sm leading-6 text-slate-700'
22
+ >
23
+ {{ children|safe }}
24
+ </div>
25
+ </div>
26
+ </div>
27
+ {% if tone == "urgent" %}
28
+ <p
29
+ class='mt-3 text-xs font-medium uppercase tracking-[0.24em] text-rose-700'
30
+ >
31
+ Action recommended
32
+ </p>
33
+ {% endif %}
34
+ </aside>
@@ -0,0 +1,42 @@
1
+ <li
2
+ class='list-none'
3
+ role='option'
4
+ tabIndex='{{ -1 }}'
5
+ aria-selected='{{ selected }}'
6
+ >
7
+ <a
8
+ href='{{ href }}'
9
+ class='group block rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:shadow-sm'
10
+ >
11
+ <div
12
+ class='flex items-start gap-3'
13
+ >
14
+ {% let className = "mt-0.5 h-5 w-5 shrink-0 text-slate-500" %}
15
+ {% let tone = "slate" %}
16
+ {% let size = "18" %}
17
+ {% let name = icon %}
18
+ {% include "./includes/glyph.html" %}
19
+ <div
20
+ class='min-w-0 flex-1'
21
+ >
22
+ {% if eyebrow_html != null %}
23
+ <div
24
+ class='truncate text-xs font-medium uppercase tracking-[0.2em] text-slate-500'
25
+ >
26
+ {{ eyebrow_html|safe }}
27
+ </div>
28
+ {% endif %}
29
+ <div
30
+ class='mt-1 truncate text-sm font-semibold text-slate-900'
31
+ >
32
+ {{ title_html|safe }}
33
+ </div>
34
+ <p
35
+ class='mt-1 text-sm leading-6 text-slate-600'
36
+ >
37
+ {{ summary_html|safe }}
38
+ </p>
39
+ </div>
40
+ </div>
41
+ </a>
42
+ </li>
@@ -0,0 +1,5 @@
1
+ <section
2
+ class='mx-auto max-w-4xl space-y-6 rounded-[28px] border border-slate-200 bg-white/90 p-6 shadow-[0_24px_80px_-40px_rgba(15,23,42,0.45)]'
3
+ >
4
+ {{ children|safe }}
5
+ </section>
@@ -0,0 +1,37 @@
1
+ use askama::Template;
2
+
3
+ #[derive(Template)]
4
+ #[template(
5
+ ext = "html",
6
+ source = r#"
7
+ <figure
8
+ class='overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-sm'
9
+ >
10
+ {% if eyebrow != null %}
11
+ <div
12
+ class='border-b border-slate-200 px-5 py-3 text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500'
13
+ >
14
+ {{ eyebrow }}
15
+ </div>
16
+ {% endif %}
17
+ <div
18
+ class='bg-slate-50 p-4'
19
+ >
20
+ {{ children|safe }}
21
+ </div>
22
+ {% if caption_html != null %}
23
+ <figcaption
24
+ class='border-t border-slate-200 px-5 py-4 text-sm leading-6 text-slate-600'
25
+ >
26
+ {{ caption_html|safe }}
27
+ </figcaption>
28
+ {% endif %}
29
+ </figure>
30
+ "#
31
+ )]
32
+ pub struct MediaFrameTemplate<'a> {
33
+ pub eyebrow: &'a str,
34
+ pub captionHtml: &'a str,
35
+ pub children: &'a str,
36
+ }
37
+
@@ -0,0 +1,49 @@
1
+ use askama::Template;
2
+
3
+ #[derive(Template)]
4
+ #[template(
5
+ ext = "html",
6
+ source = r#"
7
+ <aside
8
+ class='rounded-3xl border px-5 py-4 backdrop-blur-sm {% if tone == "calm" %}border-sky-200 bg-sky-50/80{% endif %} {% if tone == "urgent" %}border-rose-200 bg-rose-50/80{% endif %} {% if tone == "success" %}border-emerald-200 bg-emerald-50/80{% endif %}'
9
+ >
10
+ <div
11
+ class='flex items-start gap-3'
12
+ >
13
+ {% let className = "mt-0.5 h-5 w-5 shrink-0" %}
14
+ {% let tone = tone %}
15
+ {% let size = "18" %}
16
+ {% let name = icon %}
17
+ {% include "./includes/glyph.html" %}
18
+ <div
19
+ class='min-w-0 flex-1'
20
+ >
21
+ <h3
22
+ class='text-sm font-semibold tracking-tight'
23
+ >
24
+ {{ title }}
25
+ </h3>
26
+ <div
27
+ class='mt-2 text-sm leading-6 text-slate-700'
28
+ >
29
+ {{ children|safe }}
30
+ </div>
31
+ </div>
32
+ </div>
33
+ {% if tone == "urgent" %}
34
+ <p
35
+ class='mt-3 text-xs font-medium uppercase tracking-[0.24em] text-rose-700'
36
+ >
37
+ Action recommended
38
+ </p>
39
+ {% endif %}
40
+ </aside>
41
+ "#
42
+ )]
43
+ pub struct NoticePanelTemplate<'a> {
44
+ pub tone: &'a str,
45
+ pub title: &'a str,
46
+ pub icon: &'a str,
47
+ pub children: &'a str,
48
+ }
49
+
@@ -0,0 +1,59 @@
1
+ use askama::Template;
2
+
3
+ #[derive(Template)]
4
+ #[template(
5
+ ext = "html",
6
+ source = r#"
7
+ <li
8
+ class='list-none'
9
+ role='option'
10
+ tabIndex='{{ -1 }}'
11
+ aria-selected='{{ selected }}'
12
+ >
13
+ <a
14
+ href='{{ href }}'
15
+ class='group block rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:shadow-sm'
16
+ >
17
+ <div
18
+ class='flex items-start gap-3'
19
+ >
20
+ {% let className = "mt-0.5 h-5 w-5 shrink-0 text-slate-500" %}
21
+ {% let tone = "slate" %}
22
+ {% let size = "18" %}
23
+ {% let name = icon %}
24
+ {% include "./includes/glyph.html" %}
25
+ <div
26
+ class='min-w-0 flex-1'
27
+ >
28
+ {% if eyebrow_html != null %}
29
+ <div
30
+ class='truncate text-xs font-medium uppercase tracking-[0.2em] text-slate-500'
31
+ >
32
+ {{ eyebrow_html|safe }}
33
+ </div>
34
+ {% endif %}
35
+ <div
36
+ class='mt-1 truncate text-sm font-semibold text-slate-900'
37
+ >
38
+ {{ title_html|safe }}
39
+ </div>
40
+ <p
41
+ class='mt-1 text-sm leading-6 text-slate-600'
42
+ >
43
+ {{ summary_html|safe }}
44
+ </p>
45
+ </div>
46
+ </div>
47
+ </a>
48
+ </li>
49
+ "#
50
+ )]
51
+ pub struct ResourceTileTemplate<'a> {
52
+ pub href: &'a str,
53
+ pub titleHtml: &'a str,
54
+ pub summaryHtml: &'a str,
55
+ pub eyebrowHtml: &'a str,
56
+ pub icon: &'a str,
57
+ pub selected: bool,
58
+ }
59
+
@@ -0,0 +1,17 @@
1
+ use askama::Template;
2
+
3
+ #[derive(Template)]
4
+ #[template(
5
+ ext = "html",
6
+ source = r#"
7
+ <section
8
+ class='mx-auto max-w-4xl space-y-6 rounded-[28px] border border-slate-200 bg-white/90 p-6 shadow-[0_24px_80px_-40px_rgba(15,23,42,0.45)]'
9
+ >
10
+ {{ children|safe }}
11
+ </section>
12
+ "#
13
+ )]
14
+ pub struct StackShellTemplate<'a> {
15
+ pub children: &'a str,
16
+ }
17
+
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type BadgeChipProps = {
5
+ label: string;
6
+ tone: string;
7
+ };
8
+
9
+ export default function BadgeChip({ label, tone }: BadgeChipProps) {
10
+ return (
11
+ <span
12
+ className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold tracking-wide ${(tone === "neutral") ? "bg-slate-100 text-slate-700" : ""} ${(tone === "accent") ? "bg-amber-100 text-amber-700" : ""}`}
13
+ >
14
+ {label}
15
+ </span>
16
+ );
17
+ }
18
+
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type ComparisonTableProps = {
5
+ head: string[];
6
+ rows: string[][];
7
+ };
8
+
9
+ export default function ComparisonTable({ head, rows }: ComparisonTableProps) {
10
+ return (
11
+ <div
12
+ className="overflow-x-auto rounded-3xl border border-slate-200 bg-white"
13
+ >
14
+ <table
15
+ className="min-w-full divide-y divide-slate-200"
16
+ >
17
+ <thead>
18
+ <tr>
19
+ {(head ?? []).map((cell, __index) => (
20
+ <>
21
+ <th
22
+ className="bg-slate-50 px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-slate-500"
23
+ >
24
+ {cell}
25
+ </th>
26
+ </>
27
+ ))}
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ {(rows ?? []).map((row, __index) => (
32
+ <>
33
+ <tr
34
+ className="odd:bg-white even:bg-slate-50/60"
35
+ >
36
+ {(row ?? []).map((cell, __index) => (
37
+ <>
38
+ <td
39
+ className="px-4 py-3 text-sm text-slate-700"
40
+ >
41
+ {cell}
42
+ </td>
43
+ </>
44
+ ))}
45
+ </tr>
46
+ </>
47
+ ))}
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ );
52
+ }
53
+
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type GlyphProps = {
5
+ className: string;
6
+ tone: string;
7
+ size: string;
8
+ name: string;
9
+ };
10
+
11
+ export default function Glyph({ className, tone, size, name }: GlyphProps) {
12
+ return (
13
+ <svg
14
+ className={className}
15
+ data-tone={tone}
16
+ data-size={size}
17
+ data-name={name}
18
+ />
19
+ );
20
+ }
21
+
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type HoverNoteProps = {
5
+ label: string;
6
+ children?: ReactNode;
7
+ };
8
+
9
+ export default function HoverNote({ label, children }: HoverNoteProps) {
10
+ return (
11
+ <span
12
+ data-hover-note={label}
13
+ className="cursor-help underline decoration-dotted underline-offset-4"
14
+ >
15
+ {children}
16
+ </span>
17
+ );
18
+ }
19
+
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type MediaFrameProps = {
5
+ eyebrow?: string;
6
+ caption_html?: string;
7
+ children?: ReactNode;
8
+ };
9
+
10
+ export default function MediaFrame({ eyebrow, caption_html, children }: MediaFrameProps) {
11
+ return (
12
+ <figure
13
+ className="overflow-hidden rounded-[28px] border border-slate-200 bg-white shadow-sm"
14
+ >
15
+ {(eyebrow !== null) && (
16
+ <>
17
+ <div
18
+ className="border-b border-slate-200 px-5 py-3 text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500"
19
+ >
20
+ {eyebrow}
21
+ </div>
22
+ </>
23
+ )}
24
+ <div
25
+ className="bg-slate-50 p-4"
26
+ >
27
+ {children}
28
+ </div>
29
+ {(caption_html !== null) && (
30
+ <>
31
+ <figcaption
32
+ className="border-t border-slate-200 px-5 py-4 text-sm leading-6 text-slate-600"
33
+ >
34
+ {caption_html}
35
+ </figcaption>
36
+ </>
37
+ )}
38
+ </figure>
39
+ );
40
+ }
41
+
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+ import Glyph from "./glyph";
3
+
4
+
5
+ export type NoticePanelProps = {
6
+ tone: string;
7
+ title: string;
8
+ icon: string;
9
+ children?: ReactNode;
10
+ };
11
+
12
+ export default function NoticePanel({ tone, title, icon, children }: NoticePanelProps) {
13
+ return (
14
+ <aside
15
+ className={`rounded-3xl border px-5 py-4 backdrop-blur-sm ${(tone === "calm") ? "border-sky-200 bg-sky-50/80" : ""} ${(tone === "urgent") ? "border-rose-200 bg-rose-50/80" : ""} ${(tone === "success") ? "border-emerald-200 bg-emerald-50/80" : ""}`}
16
+ >
17
+ <div
18
+ className="flex items-start gap-3"
19
+ >
20
+ <Glyph
21
+ className="mt-0.5 h-5 w-5 shrink-0"
22
+ tone={tone}
23
+ size="18"
24
+ name={icon}
25
+ />
26
+ <div
27
+ className="min-w-0 flex-1"
28
+ >
29
+ <h3
30
+ className="text-sm font-semibold tracking-tight"
31
+ >
32
+ {title}
33
+ </h3>
34
+ <div
35
+ className="mt-2 text-sm leading-6 text-slate-700"
36
+ >
37
+ {children}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ {(tone === "urgent") && (
42
+ <>
43
+ <p
44
+ className="mt-3 text-xs font-medium uppercase tracking-[0.24em] text-rose-700"
45
+ >
46
+ Action recommended
47
+ </p>
48
+ </>
49
+ )}
50
+ </aside>
51
+ );
52
+ }
53
+
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+ import Glyph from "./glyph";
3
+
4
+
5
+ export type ResourceTileProps = {
6
+ href: string;
7
+ title_html: string;
8
+ summary_html: string;
9
+ eyebrow_html?: string;
10
+ icon: string;
11
+ selected: boolean;
12
+ };
13
+
14
+ export default function ResourceTile({ href, title_html, summary_html, eyebrow_html, icon, selected }: ResourceTileProps) {
15
+ return (
16
+ <li
17
+ className="list-none"
18
+ role="option"
19
+ tabIndex={-1}
20
+ aria-selected={selected}
21
+ >
22
+ <a
23
+ href={href}
24
+ className="group block rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:shadow-sm"
25
+ >
26
+ <div
27
+ className="flex items-start gap-3"
28
+ >
29
+ <Glyph
30
+ className="mt-0.5 h-5 w-5 shrink-0 text-slate-500"
31
+ tone="slate"
32
+ size="18"
33
+ name={icon}
34
+ />
35
+ <div
36
+ className="min-w-0 flex-1"
37
+ >
38
+ {(eyebrow_html !== null) && (
39
+ <>
40
+ <div
41
+ className="truncate text-xs font-medium uppercase tracking-[0.2em] text-slate-500"
42
+ >
43
+ {eyebrow_html}
44
+ </div>
45
+ </>
46
+ )}
47
+ <div
48
+ className="mt-1 truncate text-sm font-semibold text-slate-900"
49
+ >
50
+ {title_html}
51
+ </div>
52
+ <p
53
+ className="mt-1 text-sm leading-6 text-slate-600"
54
+ >
55
+ {summary_html}
56
+ </p>
57
+ </div>
58
+ </div>
59
+ </a>
60
+ </li>
61
+ );
62
+ }
63
+
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type StackShellProps = {
5
+ children?: ReactNode;
6
+ };
7
+
8
+ export default function StackShell({ children }: StackShellProps) {
9
+ return (
10
+ <section
11
+ className="mx-auto max-w-4xl space-y-6 rounded-[28px] border border-slate-200 bg-white/90 p-6 shadow-[0_24px_80px_-40px_rgba(15,23,42,0.45)]"
12
+ >
13
+ {children}
14
+ </section>
15
+ );
16
+ }
17
+
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type BadgeChipProps = {
5
+ label: string;
6
+ tone: string;
7
+ };
8
+
9
+ export default function BadgeChip({ label, tone }: BadgeChipProps) {
10
+ return (
11
+ <span
12
+ className={["inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold tracking-wide", (tone === "neutral") ? "bg-slate-100 text-slate-700" : null, (tone === "accent") ? "bg-amber-100 text-amber-700" : null].filter(Boolean).join(" ")}
13
+ >
14
+ {label}
15
+ </span>
16
+ );
17
+ }
18
+
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type ComparisonTableProps = {
5
+ head: string[];
6
+ rows: string[][];
7
+ };
8
+
9
+ export default function ComparisonTable({ head, rows }: ComparisonTableProps) {
10
+ return (
11
+ <div
12
+ className="overflow-x-auto rounded-3xl border border-slate-200 bg-white"
13
+ >
14
+ <table
15
+ className="min-w-full divide-y divide-slate-200"
16
+ >
17
+ <thead>
18
+ <tr>
19
+ {(head ?? []).map((cell, __index) => (
20
+ <>
21
+ <th
22
+ className="bg-slate-50 px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.2em] text-slate-500"
23
+ >
24
+ {cell}
25
+ </th>
26
+ </>
27
+ ))}
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ {(rows ?? []).map((row, __index) => (
32
+ <>
33
+ <tr
34
+ className="odd:bg-white even:bg-slate-50/60"
35
+ >
36
+ {(row ?? []).map((cell, __index) => (
37
+ <>
38
+ <td
39
+ className="px-4 py-3 text-sm text-slate-700"
40
+ >
41
+ {cell}
42
+ </td>
43
+ </>
44
+ ))}
45
+ </tr>
46
+ </>
47
+ ))}
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ );
52
+ }
53
+
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type GlyphProps = {
5
+ className: string;
6
+ tone: string;
7
+ size: string;
8
+ name: string;
9
+ };
10
+
11
+ export default function Glyph({ className, tone, size, name }: GlyphProps) {
12
+ return (
13
+ <svg
14
+ className={className}
15
+ data-tone={tone}
16
+ data-size={size}
17
+ data-name={name}
18
+ />
19
+ );
20
+ }
21
+
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from "react";
2
+
3
+
4
+ export type HoverNoteProps = {
5
+ label: string;
6
+ children?: ReactNode;
7
+ };
8
+
9
+ export default function HoverNote({ label, children }: HoverNoteProps) {
10
+ return (
11
+ <span
12
+ data-hover-note={label}
13
+ className="cursor-help underline decoration-dotted underline-offset-4"
14
+ >
15
+ {children}
16
+ </span>
17
+ );
18
+ }
19
+