@kwtv/components 0.7.2 → 0.9.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,112 @@
1
+ import "./OrderTimeline.css";
2
+
3
+ /**
4
+ * Order timeline. Where one order is, for the customer: paid, being prepared,
5
+ * ready for pickup, picked up. One step per row, top to bottom, each with the
6
+ * date it happened. A React component because account pages are rendered in
7
+ * the browser from live data.
8
+ *
9
+ * A step is done, current or still to come. "stopped" ends the line for an
10
+ * order that will not go on, such as a cancelled one; the steps after it are
11
+ * left out by the consumer. Every state is a mark plus words, never colour
12
+ * alone: the current step carries aria-current="step" and the state is spelled
13
+ * out for screen readers.
14
+ */
15
+
16
+ export type OrderStepState = "done" | "current" | "todo" | "stopped";
17
+
18
+ export interface OrderStep {
19
+ label: string;
20
+ state: OrderStepState;
21
+ /** When it happened, e.g. "4 januari 2026, 14:32". Empty for steps to come. */
22
+ date?: string;
23
+ /** One line under the date, such as where to pick up. */
24
+ note?: string;
25
+ }
26
+
27
+ export interface OrderTimelineProps {
28
+ steps: OrderStep[];
29
+ /** Names the list, e.g. "Verloop van bestelling #1088". */
30
+ label: string;
31
+ /** Words read after each step's label. */
32
+ stateWords?: Record<OrderStepState, string>;
33
+ className?: string;
34
+ }
35
+
36
+ const defaultWords: Record<OrderStepState, string> = {
37
+ done: "afgerond",
38
+ current: "nu",
39
+ todo: "nog niet",
40
+ stopped: "gestopt",
41
+ };
42
+
43
+ function Mark({ state }: { state: OrderStepState }) {
44
+ if (state === "done") {
45
+ return (
46
+ <svg
47
+ width="12"
48
+ height="12"
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ stroke="currentColor"
52
+ strokeWidth="3"
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ >
56
+ <path d="M5 13l4 4L19 7" />
57
+ </svg>
58
+ );
59
+ }
60
+ if (state === "stopped") {
61
+ return (
62
+ <svg
63
+ width="12"
64
+ height="12"
65
+ viewBox="0 0 24 24"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ strokeWidth="3"
69
+ strokeLinecap="round"
70
+ >
71
+ <path d="M7 7l10 10M17 7L7 17" />
72
+ </svg>
73
+ );
74
+ }
75
+ return null;
76
+ }
77
+
78
+ export function OrderTimeline({
79
+ steps,
80
+ label,
81
+ stateWords = defaultWords,
82
+ className,
83
+ }: OrderTimelineProps) {
84
+ return (
85
+ <ol
86
+ className={["ui-otime", className].filter(Boolean).join(" ")}
87
+ aria-label={label}
88
+ >
89
+ {steps.map((step, i) => (
90
+ <li
91
+ key={i}
92
+ className={`ui-otime__step is-${step.state}`}
93
+ aria-current={step.state === "current" ? "step" : undefined}
94
+ >
95
+ <span className="ui-otime__dot" aria-hidden="true">
96
+ <Mark state={step.state} />
97
+ </span>
98
+ <span className="ui-otime__text">
99
+ <b className="ui-otime__label">
100
+ {step.label}
101
+ <span className="ui-sr-only">, {stateWords[step.state]}</span>
102
+ </b>
103
+ {step.date && <span className="ui-otime__date">{step.date}</span>}
104
+ {step.note && <span className="ui-otime__note">{step.note}</span>}
105
+ </span>
106
+ </li>
107
+ ))}
108
+ </ol>
109
+ );
110
+ }
111
+
112
+ export default OrderTimeline;
@@ -158,6 +158,9 @@
158
158
  border: 1px solid transparent;
159
159
  border-radius: var(--radius-md);
160
160
  color: var(--text);
161
+ /* IconButton renders a link when it has an href. Keep the count pill
162
+ free of the link underline. */
163
+ text-decoration: none;
161
164
  cursor: pointer;
162
165
  transition:
163
166
  background-color var(--duration-fast) var(--ease-standard),
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Feedback styles as plain, importable CSS. This is the single copy of the
3
+ * .ui-alert, .ui-empty and .ui-skel rules. Alert.astro, EmptyState.astro and
4
+ * Skeleton.astro import it themselves, so a pure-Astro app needs nothing extra.
5
+ * Bundlers emit it once, however many components and pages pull it in.
6
+ *
7
+ * React and Preact islands cannot render Astro components. An island that
8
+ * shows an alert, an empty state or a loading skeleton writes the same markup
9
+ * as the Astro component and imports this sheet once:
10
+ *
11
+ * import "@kwtv/components/styles/feedback.css";
12
+ *
13
+ * Import `@kwtv/tokens/tokens.css` before this file.
14
+ */
15
+
16
+ @layer reset, tokens, base, components, motion, utilities;
17
+
18
+ @layer components {
19
+ /* ---- Alert ---------------------------------------------------------------- */
20
+ .ui-alert {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--space-2-5, 0.625rem);
24
+ padding: var(--space-3) var(--space-4);
25
+ border-left: 3px solid currentColor;
26
+ border-radius: var(--radius-md);
27
+ font-family: var(--font-sans);
28
+ font-size: var(--text-sm);
29
+ line-height: var(--leading-sm);
30
+ }
31
+
32
+ .ui-alert__icon {
33
+ display: inline-flex;
34
+ flex-shrink: 0;
35
+ align-self: flex-start;
36
+ margin-top: calc((var(--leading-sm, 1.375rem) - 16px) / 2);
37
+ }
38
+
39
+ /* Body keeps full strength; the left rule carries the tone. */
40
+ .ui-alert__body {
41
+ color: var(--text);
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: var(--space-1);
45
+ }
46
+
47
+ .ui-alert__title {
48
+ font-weight: var(--weight-semibold);
49
+ }
50
+
51
+ .ui-alert--info {
52
+ background: var(--surface-accent);
53
+ color: var(--surface-accent-fg, var(--primary));
54
+ }
55
+
56
+ .ui-alert--success {
57
+ background: var(--success-bg);
58
+ color: var(--success);
59
+ }
60
+
61
+ .ui-alert--warning {
62
+ background: var(--warning-bg);
63
+ color: var(--warning);
64
+ }
65
+
66
+ .ui-alert--danger {
67
+ background: var(--danger-bg);
68
+ color: var(--danger);
69
+ }
70
+
71
+ /* ---- Empty state ---------------------------------------------------------- */
72
+ .ui-empty {
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: flex-start;
76
+ gap: var(--space-2);
77
+ padding: var(--space-8) var(--space-6);
78
+ border: 1px dashed var(--border-strong);
79
+ border-radius: var(--radius-lg);
80
+ background: var(--surface);
81
+ }
82
+
83
+ .ui-empty__title {
84
+ font-size: var(--text-lg);
85
+ font-weight: var(--weight-semibold);
86
+ color: var(--text);
87
+ }
88
+
89
+ .ui-empty__body {
90
+ font-size: var(--text-sm);
91
+ line-height: var(--leading-sm);
92
+ color: var(--text-muted);
93
+ }
94
+
95
+ /* The flex gap above spaces the action from the body. No extra margin. */
96
+
97
+ /* ---- Skeleton ------------------------------------------------------------- */
98
+ .ui-skel {
99
+ display: block;
100
+ background: var(--surface-hover);
101
+ background-image: linear-gradient(
102
+ 90deg,
103
+ var(--surface-hover) 0%,
104
+ var(--border) 50%,
105
+ var(--surface-hover) 100%
106
+ );
107
+ background-size: 200% 100%;
108
+ border-radius: var(--radius-md);
109
+ animation: ui-skel-shimmer 1400ms ease-in-out infinite;
110
+ }
111
+
112
+ .ui-skel--img {
113
+ width: 100%;
114
+ aspect-ratio: 4 / 3;
115
+ border-radius: 0;
116
+ }
117
+
118
+ .ui-skel--title {
119
+ height: 0.875rem;
120
+ border-radius: var(--radius-sm);
121
+ }
122
+
123
+ .ui-skel--line {
124
+ height: 0.625rem;
125
+ border-radius: var(--radius-sm);
126
+ }
127
+
128
+ .ui-skel--block {
129
+ height: 1rem;
130
+ }
131
+
132
+ @keyframes ui-skel-shimmer {
133
+ 0% {
134
+ background-position: 200% 0;
135
+ }
136
+ 100% {
137
+ background-position: -200% 0;
138
+ }
139
+ }
140
+
141
+ /* The shimmer stops for readers who ask for reduced motion. */
142
+ @media (prefers-reduced-motion: reduce) {
143
+ .ui-skel {
144
+ animation: none;
145
+ background-image: none;
146
+ }
147
+ }
148
+ }