tamga-ui 0.5.9 → 0.7.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 (47) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/blocks/danger-zone.d.ts +54 -0
  3. package/dist/blocks/danger-zone.d.ts.map +1 -0
  4. package/dist/blocks/danger-zone.js +18 -0
  5. package/dist/blocks/index.d.ts +1 -0
  6. package/dist/blocks/index.d.ts.map +1 -1
  7. package/dist/blocks/index.js +1 -0
  8. package/dist/components/appearance.d.ts +91 -5
  9. package/dist/components/appearance.d.ts.map +1 -1
  10. package/dist/components/appearance.js +164 -18
  11. package/dist/components/button.d.ts +2 -1
  12. package/dist/components/button.d.ts.map +1 -1
  13. package/dist/components/button.js +1 -1
  14. package/dist/components/chrome.d.ts +10 -1
  15. package/dist/components/chrome.d.ts.map +1 -1
  16. package/dist/components/chrome.js +2 -2
  17. package/dist/components/date-picker.d.ts +142 -4
  18. package/dist/components/date-picker.d.ts.map +1 -1
  19. package/dist/components/date-picker.js +331 -40
  20. package/dist/components/disclosure.d.ts +37 -2
  21. package/dist/components/disclosure.d.ts.map +1 -1
  22. package/dist/components/disclosure.js +13 -3
  23. package/dist/components/display.d.ts +35 -6
  24. package/dist/components/display.d.ts.map +1 -1
  25. package/dist/components/display.js +21 -7
  26. package/dist/components/input.d.ts +2 -1
  27. package/dist/components/input.d.ts.map +1 -1
  28. package/dist/components/primitives.d.ts +3 -1
  29. package/dist/components/primitives.d.ts.map +1 -1
  30. package/dist/components/primitives.js +2 -2
  31. package/dist/fonts.css +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/kit.css +178 -3
  36. package/dist/lib/image.d.ts +2 -0
  37. package/dist/lib/image.d.ts.map +1 -1
  38. package/dist/lib/image.js +1 -1
  39. package/dist/lib/uyar.d.ts +2 -0
  40. package/dist/lib/uyar.d.ts.map +1 -0
  41. package/dist/lib/uyar.js +10 -0
  42. package/dist/patterns/app-shell.d.ts +20 -1
  43. package/dist/patterns/app-shell.d.ts.map +1 -1
  44. package/dist/patterns/app-shell.js +37 -3
  45. package/dist/styles.css +1 -1
  46. package/dist/theme.css +8 -1
  47. package/package.json +10 -12
package/CHANGELOG.md CHANGED
@@ -5,6 +5,162 @@ Versions follow [semver](https://semver.org/). Through `0.x`, breaking changes m
5
5
 
6
6
  > 🇹🇷 Türkçe için [tamga.org.tr/tr](https://tamga.org.tr/tr).
7
7
 
8
+ ## 0.7.0
9
+
10
+ > A calendar can say no and carry the hour; a section can be told whether it is open; a label can
11
+ > carry the user's colour; the settings page has a bottom; the image field can talk to a server.
12
+
13
+ ### Added
14
+
15
+ - **`DatePicker` / `Calendar` `min` and `max`** (`DateISO`). Days outside the range are disabled
16
+ and announced as such, the arrow keys skip them, and a month button closes when the whole
17
+ month is out of reach. A product had fallen back to `<input type="date" min=…>` for "not before
18
+ today"; that field looks different in every browser and lets the user type a date the kit had
19
+ decided should only be picked.
20
+ - **Calendar keyboard and grid semantics.** One day is in the tab order; the arrows move between
21
+ days (±1, ±7) across month edges, `Home`/`End` to the week's edges, `PageUp`/`PageDown` across
22
+ months; disabled days are skipped and the stop never lands on one (it follows the selected
23
+ day, else the first open day, and moves with an external `value`). `role="grid"` with rows
24
+ and `gridcell`s, every day named with its full date, `aria-selected` on the chosen day, native
25
+ `disabled` only.
26
+ - **`DateTimePicker`.** Date plus time, picked not typed: a calendar and hour/minute selects
27
+ (AM/PM only in 12-hour locales; the words from `Intl`, else `labels.am`/`labels.pm`, never
28
+ the kit's own English). The value is `DateTimeISO`, an instant written with the offset of the
29
+ picker's `timeZone` at that instant, `2026-04-15T14:30:00+03:00`; send
30
+ `new Date(v).toISOString()` to the server. A separate component rather than a prop: the
31
+ value's shape is different, and an `onChange` whose type depends on a prop is a trap.
32
+ `labels` adds `hour`, `minute`, `period`.
33
+ - `timeZone` (IANA) on `Calendar`, `DatePicker` and `DateTimePicker`. Wall clock and instant
34
+ meet through `Intl`; "today", `min`/`max`, the field's text and the emitted offset all use
35
+ that zone, never the browser's. Give it explicitly in a server-rendered app. DST: an hour
36
+ inside a spring gap is not offered and a wall time in it is pushed forward; an autumn
37
+ ambiguous hour is written with the later, standard offset, while a value coming from
38
+ outside keeps its own offset, also when only its minute changes. Checked against an
39
+ independent oracle over the 2026 transitions of 418 zones.
40
+ - Every emitted value is inside `[min, max]`: day, hour, minute and AM/PM changes are clamped
41
+ to the nearest valid time and the selects follow; hours with no valid minute are not
42
+ offered. A stored minute off the `minuteStep` grid, or a stored value outside the range, is
43
+ shown as it is rather than rounded or hidden: the field never lies about a stored value.
44
+ The hour boxes stay locked until a day is picked, so an hour alone never emits today.
45
+ `minuteStep` below 1 reads as 1.
46
+ - `id` and `invalid` on both pickers, like `Input`. An invalid `timeZone` falls back to the
47
+ browser's and an unparseable `value` / `min` / `max` reads as empty, with a console warning
48
+ in development, instead of a `RangeError`.
49
+ - **`Collapsible` controlled mode** · `open` + `onOpenChange(open)`. With `open` the product
50
+ holds the state and a click only reports; without it the section keeps its own, as before.
51
+ Both given, `open` wins and `defaultOpen` is ignored (React's contract). A product had rewritten
52
+ the row by hand because it needed to know when a row opened: it fetches on open.
53
+ - **`title` takes a node.** "Accessibility · /pricing" with the path faint; the accessible
54
+ name is `title` and `meta` together.
55
+ - **`note`** · a quiet line that stays visible while the section is closed, between head and
56
+ body, inside the same frame, bound to the head with `aria-describedby`: why a measurement
57
+ did not count, when it last ran.
58
+ - **`unmountOnClose`** · off by default (the body stays in the DOM, `hidden`); on, a heavy body
59
+ is not rendered while closed and `aria-controls` is omitted meanwhile.
60
+ - The caret's turn honours `prefers-reduced-motion`.
61
+ - **`Tag` `swatch`** · the user's own colour as a small mark before the text, `aria-hidden`.
62
+ Hex only (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`); anything else draws no mark, so user data
63
+ reaches `style` only as a validated value. This is the one **written** exception to Law 3
64
+ ("colour is for deviation and interaction"): the colour is the user's data, an identity mark,
65
+ never a ground or a text colour, never status. The ring around the mark derives from the ink
66
+ (from the page colour inside `look="solid"`), so a near-white or near-black choice stays
67
+ visible in both themes. Written into the physics page and `docs/gerekce/06`, so nobody deletes
68
+ it as a violation or cites it as a precedent. Without `swatch` the output is unchanged.
69
+ - **`ColorMark`** · the mark on its own, on the kit's `tamga-mark` family, for a clickable
70
+ suggestion inside a `Button`.
71
+ - **`ColorSwatches` `noneLabel`** · a "no colour" option; an empty value selects it instead
72
+ of lighting the custom box.
73
+ - **`DangerZone` block** (`tamga-ui/blocks`) · the irreversible actions at the bottom of a
74
+ settings page. A title, an optional description and one or more action rows: an optional
75
+ name, one sentence, a `danger` button. `disabledReason` closes a row and shows the reason as
76
+ **visible text** bound to the button with `aria-describedby` · a disabled button is not a
77
+ reason, and a tooltip on one is invisible to most browsers, the keyboard and touch. `warning`
78
+ takes the kit's `Alert` when a precondition stopped the action; `busy` + `busyLabel` keep the
79
+ button named while it runs. The block never opens a confirm dialog: the click is the caller's,
80
+ the product builds `ConfirmDialog`. Edge and glyph tile in the critical tone, not the whole
81
+ card; the heading and the warning glyph say "danger" without colour. `data-action` sits on the
82
+ button, `data-key` on the row; every row takes `data-*`. A product had hand-written this zone
83
+ three ways on three pages.
84
+ - **`ImageField` upload path** · `onUpload(file) => Promise<string | false>` and
85
+ `onRemove() => Promise<boolean | void>`. Given `onUpload`, the field no longer converts to a
86
+ data URL: it hands the `File` to the caller and carries the states. Waiting: the input and
87
+ every button lock, the button is named by `labels.uploading` / `labels.removing`. Failure: the
88
+ rejected reason (a string, an `Error`, or `{ message }`) under the field in a `role="alert"`
89
+ region, shown as it is, so map a raw network error to your own words first; an empty reason
90
+ shows the new `labels.errorUpload`. The chosen file and its preview stay, "confirm" retries.
91
+ Resolving `onUpload` with `false` keeps the kit silent, for a caller that already handled the
92
+ value or the error (a plan-wall dialog); any other non-string result warns in development,
93
+ because a forgotten `return` must never read as "uploaded". `onRemove` resolving `false` is
94
+ silent the same way. Success: the returned address goes to `onChange`, and a quiet inline
95
+ `role="status"` line (`labels.uploaded` / `labels.removed`). Both live regions are in the DOM
96
+ from the start, only their text changes. `confirm` previews the pick and waits for a click
97
+ (for a logo); the default uploads on pick (for an avatar). `accept` and `maxBytes` are
98
+ checked on both paths before anything else runs, an empty file is refused with
99
+ `errorUnreadable`; `disabled` + `lockedNote` show the reason as visible text wired to the
100
+ buttons with `aria-describedby`. The preview is an object URL, revoked on cancel, on a new
101
+ pick, on success, when `value` changes from outside, and on unmount; a result arriving after
102
+ unmount or after an outside `value` change is dropped, while the caller's own `onChange` with
103
+ the same result (a cache update inside `onUpload`) is not "outside". `maxEdge` is optional in
104
+ the type: not used with `onUpload`, and without either the image is kept at its own size.
105
+ `labels.empty` takes a `ReactNode`. The pre-existing synchronous path (no `onUpload`) renders
106
+ the same markup, locked by test; its only change is that a file outside `accept` is refused
107
+ before `prepareImage` (the input's own list said so already). A product had written this
108
+ field four times.
109
+ - **`LogoTile` `surface`** · `"auto" | "light" | "dark"`, default `auto` (today's behaviour,
110
+ follows the page). `dark` draws the dark theme's ground even on a light page, for a logo drawn
111
+ for dark; `light` the reverse. Ground, edge and initial come as a fixed trio of tokens
112
+ (`--color-logo-ground/edge/ink-light|dark`), identical in both themes, so the tile reads the
113
+ same at night. Measured: initial 6.6:1 on light, 5.9:1 on dark; edge 3.9:1 on dark
114
+ (WCAG 1.4.11). A product that had been wrapping the tile in a local `.dark` class to get this
115
+ can drop the wrapper · note the ground differs by one step: the new dark surface is the dark
116
+ theme's **card** (`#16233a`), the old wrapper with `bg-page` gave the dark **page** (`#101a2c`).
117
+ Not pixel-identical; the card is what a tile sits on.
118
+ - **`Select` `disabled`** · locks the trigger, and looks locked (sunk ground, dashed edge, faint
119
+ ink).
120
+ - `check-item-hooks` also reads `actions` lists, and no longer mistakes `=>` for a closing
121
+ bracket (it was cutting item types short before their `Record<string, unknown>`).
122
+
123
+ ### Changed
124
+
125
+ - The calendar's keyboard model: one tab stop and arrow keys instead of a tab stop on every day.
126
+ A `DatePicker` without `min`/`max` renders the same, but Tab now leaves the grid after one
127
+ stop; the arrows move inside it.
128
+
129
+ ### Fixed
130
+
131
+ - **`DatePicker` `disabled`** left the clear and open buttons live: a locked date could be
132
+ cleared. Both buttons are disabled with the field.
133
+ - **`Button` busy without `busyLabel`** had no accessible name: the label was `invisible` and
134
+ `aria-hidden`. The button's own text now stays in the tree as sr-only, as the docs claimed.
135
+ - **Props tables on the docs site** · `ErrorState` lacked `onRetry` / `retryLabel` (a
136
+ parenthesised union was not read), `Button` lacked `busy` / `busyLabel` and `Input` `leading`
137
+ (an alias body was cut at its first `;`), and tables carried rows from types named inside a
138
+ prop (`AppShell` showed `NavEntry`'s fields). Only top-level intersections expand now, and a
139
+ row type such as `DangerAction` gets its own table.
140
+
141
+ ## 0.6.0
142
+
143
+ > Sections fold; the one you are in never does.
144
+
145
+ ### Added
146
+
147
+ - **`AppShell` section headers fold.** In the wide rail every section header is now a button:
148
+ click it and the section's entries slide shut; click again and they return. A customer with
149
+ eight sections asked for exactly this — the rail had become a wall. Three rules keep it honest:
150
+ the open page's section never folds (you can always read where you are), a route that lands
151
+ inside a folded section finds it open, and the narrow rail is untouched (nothing to fold when
152
+ there are no headers). New props: `collapsibleSections` (default `true`), `collapsedSections`
153
+ for a controlled rail, `onSectionToggle(section, open)`. Where the preference lives — browser,
154
+ account, device — is the product's call; the shell never writes storage.
155
+
156
+ ### Breaking
157
+
158
+ - Section headers in the wide rail are `<button aria-expanded>` elements instead of `<p>`, and
159
+ each section's links now sit inside a `.tamga-rail-group-body` wrapper rather than directly
160
+ under `<nav>`. A selector written as `nav > .tamga-rail-link` or a test that read the header
161
+ as a paragraph needs updating; `data-nav` on every link is unchanged and is the supported hook.
162
+ Pass `collapsibleSections={false}` to keep plain headers (the wrapper stays either way).
163
+
8
164
  ## 0.5.9
9
165
 
10
166
  > The hook lands on the root, in every branch.
@@ -0,0 +1,54 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * TEHLİKELİ BÖLGE · ayarlar sayfasının en altı, geri alınamayan eylemler.
4
+ *
5
+ * Üç ayar sayfası aynı bölgeyi üç ayrı biçimde elle yazıyordu; birinde kapalı
6
+ * düğmenin gerekçesi yalnız `title=` ipucundaydı. Blok onay penceresi AÇMAZ:
7
+ * tık çağırana gider, `ConfirmDialog`u ürün kurar · "adını yazdır" gibi
8
+ * kararlar onda kalır. Gerekçe: docs/gerekce/08-blok-ve-sablon.md
9
+ */
10
+ export type DangerAction = {
11
+ key: string;
12
+ /**
13
+ * The row's title. Optional: when the button's label already names the action ("Delete the
14
+ * customer"), a title would repeat it, and three products had none. TR: Satırın başlığı.
15
+ * İsteğe bağlı: düğmenin metni eylemi zaten adlandırıyorsa ("Müşteriyi sil") başlık onu
16
+ * tekrarlardı, ve üç ürünün hiçbirinde yoktu.
17
+ */
18
+ title?: string;
19
+ /** One sentence saying what will happen. TR: Ne olacağını söyleyen tek cümle. */
20
+ description: string;
21
+ /** The button's text. TR: Düğmenin metni. */
22
+ label: string;
23
+ /** The click is the caller's; open your `ConfirmDialog` here. TR: Tık çağıranın; `ConfirmDialog`unu burada aç. */
24
+ onClick?: () => void;
25
+ /**
26
+ * Given, the row is closed and THIS SENTENCE is shown next to the button and bound to it with
27
+ * `aria-describedby`. A disabled button is not a reason; a tooltip on one is invisible to most
28
+ * browsers, to the keyboard and to touch. TR: Verilirse satır kapalı ve BU CÜMLE düğmenin
29
+ * yanında görünür, `aria-describedby` ile ona bağlı. Kapalı düğme bir gerekçe değildir; üstündeki
30
+ * ipucu çoğu tarayıcıda, klavyede ve dokunmatikte görünmez.
31
+ */
32
+ disabledReason?: string;
33
+ /** The action is running: the button shows a spinner and locks. TR: Eylem sürüyor: düğme spinner gösterir ve kilitlenir. */
34
+ busy?: boolean;
35
+ /** The button's accessible name while busy ("Deleting…"); without it the name would be empty. TR: Sürerken düğmenin erişilebilir adı ("Siliniyor…"); yoksa ad boş kalırdı. */
36
+ busyLabel?: string;
37
+ /**
38
+ * Shown under the row when a precondition stopped the action: the server said "do this first".
39
+ * Put the kit's `Alert` (`state="caution"`) here. TR: Bir önkoşul eylemi durdurduğunda satırın
40
+ * altında · sunucu "önce şunu yap" dedi. Kitin `Alert`ini (`state="caution"`) koy.
41
+ */
42
+ warning?: ReactNode;
43
+ } & Record<string, unknown>;
44
+ export declare function DangerZone({ title, description, actions, className, ...rest }: {
45
+ /** The zone's heading; names the region for assistive tech. TR: Bölgenin başlığı; yardımcı teknoloji için bölgeyi adlandırır. */
46
+ title: string;
47
+ /** One line under the heading: "Nothing here can be undone." TR: Başlığın altında tek satır: "Buradaki işlemler geri alınamaz." */
48
+ description?: string;
49
+ actions: readonly DangerAction[];
50
+ className?: string;
51
+ /** `data-*` hooks pass through. TR: `data-*` kancaları geçiyor. */
52
+ [k: `data-${string}`]: unknown;
53
+ }): import("react").JSX.Element;
54
+ //# sourceMappingURL=danger-zone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"danger-zone.d.ts","sourceRoot":"","sources":["../../src/blocks/danger-zone.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAS,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAO9C;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,kHAAkH;IAClH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4HAA4H;IAC5H,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8KAA8K;IAC9K,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,EACT,GAAG,IAAI,EACR,EAAE;IACD,iIAAiI;IACjI,KAAK,EAAE,MAAM,CAAC;IACd,mIAAmI;IACnI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,CAAC,CAAC,EAAE,QAAQ,MAAM,EAAE,GAAG,OAAO,CAAC;CAChC,+BAsDA"}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { createElement as _createElement } from "react";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ import { useId } from "react";
5
+ import { Button } from "../components/button.js";
6
+ import { Icon } from "../components/icon.js";
7
+ import { Warning } from "../components/icons.js";
8
+ import { cn } from "../lib/cn.js";
9
+ import { dataProps } from "../lib/data-props.js";
10
+ export function DangerZone({ title, description, actions, className, ...rest }) {
11
+ const id = useId();
12
+ return (_jsxs("section", { ...dataProps(rest), className: cn("tamga-danger-zone", className), "aria-labelledby": `${id}-b`, children: [_jsxs("div", { className: "tamga-danger-zone-head", children: [_jsx("span", { className: "tamga-danger-zone-tile", "aria-hidden": "true", children: _jsx(Icon, { icon: Warning, size: "sm", weight: "bold" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h3", { id: `${id}-b`, className: "text-subhead font-semibold text-ink", children: title }), description ? _jsx("p", { className: "mt-1 text-small leading-relaxed text-ink-soft", children: description }) : null] })] }), _jsx("ul", { className: "tamga-danger-zone-list", children: actions.map(({ key, title: ad, description: ne, label, onClick, disabledReason, busy, busyLabel, warning, ...satir }, i) => {
13
+ const gerekceId = `${id}-g${i}`;
14
+ return (_createElement("li", { ...dataProps(satir), key: key, "data-key": key, "data-disabled": disabledReason ? "true" : undefined, className: "tamga-danger-zone-row" },
15
+ _jsxs("div", { className: "flex flex-wrap items-start gap-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [ad ? _jsx("p", { className: "text-body font-semibold text-ink", children: ad }) : null, _jsx("p", { className: ad ? "text-small leading-relaxed text-ink-soft" : "text-body leading-relaxed text-ink", children: ne }), disabledReason ? (_jsx("p", { id: gerekceId, className: "tamga-danger-zone-reason", "data-reason": true, children: disabledReason })) : null] }), _jsx(Button, { variant: "danger", size: "sm", "data-action": key, onClick: onClick, disabled: Boolean(disabledReason) || busy, busy: busy, busyLabel: busyLabel, "aria-describedby": disabledReason ? gerekceId : undefined, children: label })] }),
16
+ warning ? _jsx("div", { className: "mt-3", children: warning }) : null));
17
+ }) })] }));
18
+ }
@@ -8,4 +8,5 @@
8
8
  export { FilterBar, rangeStart, rangeEnd, type FilterBarLabels, type FilterBarProps, type FilterField, type FilterFieldKind, type FilterGroup, type FilterValues, } from "./filter-bar.js";
9
9
  export { SaveBar } from "./save-bar.js";
10
10
  export { CountRow } from "./count-row.js";
11
+ export { DangerZone, type DangerAction } from "./danger-zone.js";
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/blocks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,SAAS,EACT,UAAU,EACV,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/blocks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,SAAS,EACT,UAAU,EACV,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
@@ -8,3 +8,4 @@
8
8
  export { FilterBar, rangeStart, rangeEnd, } from "./filter-bar.js";
9
9
  export { SaveBar } from "./save-bar.js";
10
10
  export { CountRow } from "./count-row.js";
11
+ export { DangerZone } from "./danger-zone.js";
@@ -9,14 +9,23 @@ export type SwatchOption = {
9
9
  * Brand colour chosen from a few squares, or written by hand.
10
10
  * TR: Birkaç kareden seçilen ya da elle yazılan marka rengi.
11
11
  */
12
- export declare function ColorSwatches({ options, value, onChange, customLabel, custom, className, ...rest }: {
12
+ export declare function ColorSwatches({ options, value, onChange, customLabel, custom, noneLabel, className, ...rest }: {
13
13
  options: readonly SwatchOption[];
14
+ /** The chosen hex; `""` means "no colour" when `noneLabel` is given. TR: Seçili hex; `noneLabel` verilmişse `""` "renk yok" demek. */
14
15
  value: string;
16
+ /** Fires with the hex, or `""` for the "no colour" box. TR: Hex ile ateşlenir; "renk yok" kutusu için `""`. */
15
17
  onChange: (hex: string) => void;
16
18
  /** Accessible name of the "my own colour" control. TR: "Kendi rengim" kontrolünün erişilebilir adı. */
17
19
  customLabel: string;
18
20
  /** Whether a free colour is offered at all. TR: Serbest rengin hiç sunulup sunulmadığı. */
19
21
  custom?: boolean;
22
+ /**
23
+ * Given, a first box means "no colour" and an empty `value` selects it. Without it an empty
24
+ * value lit the custom box, so an optional colour read as a choice. TR: Verilirse ilk kutu
25
+ * "renk yok" demek ve boş `value` onu seçer. Yokken boş değer "kendi rengim" kutusunu
26
+ * yakıyordu, isteğe bağlı bir renk bir seçim gibi okunuyordu.
27
+ */
28
+ noneLabel?: string;
20
29
  className?: string;
21
30
  /** `data-*` hooks pass through. TR: `data-*` kancaları geçiyor. */
22
31
  [k: `data-${string}`]: unknown;
@@ -88,27 +97,104 @@ export declare function RailCards({ value, onChange, labels, className, ...rest
88
97
  * offers reorder arrows with nothing to reorder.
89
98
  * TR: `FileUpload` DEĞİL: o bir GALERİ (çoklu seçim, sıralama okları, "kapak"
90
99
  * rozeti). Ürün fotoğrafları için doğru, tek bir logo için yanlış.
100
+ *
101
+ * Two paths, one field. Without `onUpload` the image is shrunk in the browser
102
+ * and a data URL goes to `onChange`. With `onUpload` the file goes to the
103
+ * caller and the field carries the states: waiting, failure, a quiet
104
+ * confirmation. TR: İki yol, tek alan. `onUpload` yokken görsel tarayıcıda
105
+ * küçültülür, data-URL `onChange`'e gider. `onUpload` verilince dosya çağırana
106
+ * gider; alan bekleme, hata ve sessiz onayı taşır.
107
+ *
108
+ * Gerekçe: docs/gerekce/01-form-ve-girdi.md
91
109
  */
92
- export declare function ImageField({ value, onChange, maxEdge, labels, preview, extra, className, ...rest }: {
110
+ export declare function ImageField({ value, onChange, maxEdge, labels, preview, extra, onUpload, onRemove, confirm, accept, maxBytes, disabled, lockedNote, className, ...rest }: {
93
111
  value: string | null;
94
112
  onChange: (next: string | null) => void;
95
- /** Longest edge of the stored image, in pixels. TR: Saklanan görselin en uzun kenarı, piksel. */
96
- maxEdge: number;
113
+ /**
114
+ * Longest edge of the stored image, in pixels; the browser path shrinks to it, so give it
115
+ * there. Not used with `onUpload`: the server decides. TR: Saklanan görselin en uzun kenarı,
116
+ * piksel; tarayıcı yolu ona küçültür, orada ver. `onUpload` ile kullanılmaz: sunucu karar verir.
117
+ */
118
+ maxEdge?: number;
97
119
  labels: {
98
120
  /** "Logo", "Mark": used inside the buttons. TR: "Logo", "Amblem": düğmelerin içinde geçiyor. */
99
121
  name: string;
100
122
  upload: string;
101
123
  replace: string;
102
124
  remove: string;
103
- empty: string;
125
+ /** The empty state's content: a sentence, or a node. TR: Boş durumun içeriği: bir cümle ya da bir düğüm. */
126
+ empty: ReactNode;
104
127
  errorType: string;
105
128
  errorSize: string;
106
129
  errorUnreadable: string;
130
+ /**
131
+ * Shown when `onUpload` or `onRemove` rejects without a message (an empty string, `undefined`);
132
+ * falls back to `errorUnreadable`. TR: `onUpload` ya da `onRemove` mesajsız reddedince (boş
133
+ * metin, `undefined`); yoksa `errorUnreadable`.
134
+ */
135
+ errorUpload?: string;
136
+ /** The button's name while `onUpload` runs; falls back to `upload`. TR: `onUpload` sürerken düğmenin adı; yoksa `upload`. */
137
+ uploading?: string;
138
+ /** The button's name while `onRemove` runs; falls back to `remove`. TR: `onRemove` sürerken düğmenin adı; yoksa `remove`. */
139
+ removing?: string;
140
+ /** The quiet line after a successful upload; omitted, nothing is said. TR: Başarılı yüklemeden sonraki sessiz satır; yoksa susar. */
141
+ uploaded?: string;
142
+ /** The quiet line after a successful removal. TR: Başarılı kaldırmadan sonraki sessiz satır. */
143
+ removed?: string;
144
+ /** The button that sends a previewed pick; falls back to `upload`. TR: Önizlenen seçimi gönderen düğme; yoksa `upload`. */
145
+ confirm?: string;
146
+ /** The button that drops a previewed pick; falls back to `remove`. TR: Önizlenen seçimi bırakan düğme; yoksa `remove`. */
147
+ cancel?: string;
107
148
  };
108
149
  /** The caller draws the preview: a logo is wide, a mark is square. TR: Önizlemeyi çağıran çiziyor: logo geniş, amblem kare. */
109
150
  preview: (src: string) => ReactNode;
110
151
  /** An extra control beside the buttons. TR: Düğmelerin yanına giren ek kontrol. */
111
152
  extra?: ReactNode;
153
+ /**
154
+ * Hands the chosen file to the caller instead of converting it. Resolve with the stored
155
+ * address: it goes to `onChange` and `labels.uploaded` is announced. Resolve with `false` when
156
+ * you handled the result yourself (a plan-wall dialog, your own `onChange`): the kit stays
157
+ * silent. Anything else is a bug and warns in development, because a forgotten `return` must
158
+ * not read as "uploaded". Reject with a string, an `Error` or `{ message }`: that text is shown
159
+ * under the field AS IT IS, so map a raw network error to your own words before rejecting; an
160
+ * empty reason shows `labels.errorUpload`. The file and its preview stay, "confirm" retries. A
161
+ * result that arrives after `value` changed from outside, or after unmount, is dropped. TR:
162
+ * Seçilen dosyayı dönüştürmek yerine çağırana verir. Saklanan adresle çözülür: `onChange`'e
163
+ * gider, `labels.uploaded` duyurulur. Sonucu kendin ele aldıysan `false` ile çöz: kit susar.
164
+ * Başka bir değer geliştirmede uyarır; unutulan bir `return` "yüklendi" sayılmaz. Metin, `Error`
165
+ * ya da `{ message }` ile reddedilirse metin alanın altında OLDUĞU GİBİ gösterilir, ağ hatasını
166
+ * reddetmeden önce kendi sözüne çevir; boş sebep `labels.errorUpload` gösterir. Dosya ve
167
+ * önizleme kalır, "confirm" yeniden dener. `value` dışarıdan değiştikten ya da bileşen
168
+ * kalktıktan sonra gelen sonuç atılır.
169
+ */
170
+ onUpload?: (file: File) => Promise<string | false>;
171
+ /**
172
+ * Awaited before `onChange(null)`; `false` keeps the kit silent (you handled it); a rejection
173
+ * shows under the field like `onUpload`'s. TR: `onChange(null)`'dan önce beklenir; `false` kiti
174
+ * susturur (sen ele aldın); red `onUpload`'daki gibi alanın altında.
175
+ */
176
+ onRemove?: () => Promise<boolean | void>;
177
+ /**
178
+ * Preview first, send on a click: for a logo, a chance to notice the wrong file. Off, the pick
179
+ * uploads at once (an avatar). TR: Önce önizle, tıklayınca gönder: logo için yanlış dosyayı fark
180
+ * etme şansı. Kapalıyken seçim hemen gider (avatar).
181
+ */
182
+ confirm?: boolean;
183
+ /**
184
+ * The input's accept list; the pick is checked against it on both paths, before `prepareImage`
185
+ * or `onUpload`. TR: Girdinin accept listesi; seçim iki yolda da `prepareImage` ya da
186
+ * `onUpload`'dan önce buna göre denetlenir.
187
+ */
188
+ accept?: string;
189
+ /** Refused on both paths with `errorSize`; 8 MB by default. TR: İki yolda da `errorSize` ile reddedilir; varsayılan 8 MB. */
190
+ maxBytes?: number;
191
+ disabled?: boolean;
192
+ /**
193
+ * Why the field is locked, as visible text under it and `aria-describedby` on the buttons; a
194
+ * tooltip hides from touch and keyboards. TR: Alanın neden kilitli olduğu, altında görünür
195
+ * metin ve düğmelerde `aria-describedby`; ipucu dokunmatikten ve klavyeden saklanır.
196
+ */
197
+ lockedNote?: string;
112
198
  className?: string;
113
199
  /** `data-*` hooks pass through. TR: `data-*` kancaları geçiyor. */
114
200
  [k: `data-${string}`]: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/components/appearance.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/components/appearance.tsx"],"names":[],"mappings":"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"}
@@ -1,22 +1,24 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useId, useRef, useState } from "react";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { makePalette } from "../lib/palette.js";
6
6
  import { dataProps } from "../lib/data-props.js";
7
- import { IMAGE_ACCEPT, prepareImage } from "../lib/image.js";
7
+ import { IMAGE_ACCEPT, RAW_LIMIT, prepareImage } from "../lib/image.js";
8
+ import { uyar } from "../lib/uyar.js";
8
9
  import { Alert } from "./primitives.js";
9
10
  import { Button } from "./button.js";
10
11
  import { Icon } from "./icon.js";
11
- import { Check, Eyedropper } from "./icons.js";
12
+ import { Check, Eyedropper, Close } from "./icons.js";
12
13
  /**
13
14
  * Brand colour chosen from a few squares, or written by hand.
14
15
  * TR: Birkaç kareden seçilen ya da elle yazılan marka rengi.
15
16
  */
16
- export function ColorSwatches({ options, value, onChange, customLabel, custom = true, className, ...rest }) {
17
+ export function ColorSwatches({ options, value, onChange, customLabel, custom = true, noneLabel, className, ...rest }) {
17
18
  const secili = (hex) => hex.toLowerCase() === value.toLowerCase();
18
- const hazirMi = options.some((o) => secili(o.hex));
19
- return (_jsxs("span", { ...dataProps(rest), className: cn("flex flex-wrap items-center gap-3", className), children: [options.map(({ hex, label: etiket, ...rest }) => (_jsx("button", { ...rest, type: "button", className: "tamga-swatch", style: { background: hex, color: mürekkep(hex) }, "data-selected": secili(hex), "aria-pressed": secili(hex), "aria-label": etiket, title: etiket, onClick: () => onChange(hex), children: secili(hex) ? _jsx(Icon, { icon: Check, size: "base", weight: "bold" }) : null }, hex))), custom ? (_jsxs("label", { className: "tamga-swatch tamga-swatch-custom", "data-selected": !hazirMi, title: customLabel, style: hazirMi
19
+ const yokSecili = noneLabel !== undefined && value === "";
20
+ const hazirMi = yokSecili || options.some((o) => secili(o.hex));
21
+ return (_jsxs("span", { ...dataProps(rest), className: cn("flex flex-wrap items-center gap-3", className), children: [noneLabel !== undefined ? (_jsx("button", { type: "button", className: "tamga-swatch tamga-swatch-none", "data-selected": yokSecili, "aria-pressed": yokSecili, "aria-label": noneLabel, title: noneLabel, onClick: () => onChange(""), children: yokSecili ? _jsx(Icon, { icon: Check, size: "base", weight: "bold" }) : _jsx(Icon, { icon: Close, size: "base", weight: "bold" }) })) : null, options.map(({ hex, label: etiket, ...rest }) => (_jsx("button", { ...rest, type: "button", className: "tamga-swatch", style: { background: hex, color: mürekkep(hex) }, "data-selected": secili(hex), "aria-pressed": secili(hex), "aria-label": etiket, title: etiket, onClick: () => onChange(hex), children: secili(hex) ? _jsx(Icon, { icon: Check, size: "base", weight: "bold" }) : null }, hex))), custom ? (_jsxs("label", { className: "tamga-swatch tamga-swatch-custom", "data-selected": !hazirMi, title: customLabel, style: hazirMi
20
22
  ? /* Hiçbir hazır renk seçili değilken kutu o rengi giyiyor;
21
23
  seçiliyken damalı zemin "burada henüz bir renk yok" diyor. */
22
24
  { background: DAMA }
@@ -117,29 +119,173 @@ export function RailCards({ value, onChange, labels, className, ...rest }) {
117
119
  * offers reorder arrows with nothing to reorder.
118
120
  * TR: `FileUpload` DEĞİL: o bir GALERİ (çoklu seçim, sıralama okları, "kapak"
119
121
  * rozeti). Ürün fotoğrafları için doğru, tek bir logo için yanlış.
122
+ *
123
+ * Two paths, one field. Without `onUpload` the image is shrunk in the browser
124
+ * and a data URL goes to `onChange`. With `onUpload` the file goes to the
125
+ * caller and the field carries the states: waiting, failure, a quiet
126
+ * confirmation. TR: İki yol, tek alan. `onUpload` yokken görsel tarayıcıda
127
+ * küçültülür, data-URL `onChange`'e gider. `onUpload` verilince dosya çağırana
128
+ * gider; alan bekleme, hata ve sessiz onayı taşır.
129
+ *
130
+ * Gerekçe: docs/gerekce/01-form-ve-girdi.md
120
131
  */
121
- export function ImageField({ value, onChange, maxEdge, labels, preview, extra, className, ...rest }) {
132
+ export function ImageField({ value, onChange, maxEdge, labels, preview, extra, onUpload, onRemove, confirm = false, accept = IMAGE_ACCEPT, maxBytes = RAW_LIMIT, disabled = false, lockedNote, className, ...rest }) {
122
133
  const id = useId();
123
134
  const girdi = useRef(null);
124
135
  const [hata, setHata] = useState(null);
136
+ const [bekleyen, setBekleyen] = useState(null);
137
+ const [mesgul, setMesgul] = useState(null);
138
+ const [onay, setOnay] = useState(null);
139
+ /* Adres ref'te de: kalkışta son adresi bırakmak için state okunamıyor. İstek
140
+ sayacı, "canlı" bayrağı ve son değer bayat cevabı ayırıyor: kalkıştan ya da
141
+ dış değer değişiminden sonra gelen sonuç ne onChange'e gider ne ekrana. */
142
+ const adres = useRef(null);
143
+ const istek = useRef(0);
144
+ const canli = useRef(true);
145
+ const sonDeger = useRef(value);
146
+ sonDeger.current = value;
147
+ function birak() {
148
+ if (adres.current)
149
+ URL.revokeObjectURL(adres.current);
150
+ adres.current = null;
151
+ setBekleyen(null);
152
+ }
153
+ useEffect(() => {
154
+ canli.current = true;
155
+ return () => {
156
+ canli.current = false;
157
+ if (adres.current)
158
+ URL.revokeObjectURL(adres.current);
159
+ adres.current = null;
160
+ };
161
+ }, []);
162
+ /* Değer dışarıdan geldiyse bekleyen önizleme bayat kaldı. */
163
+ useEffect(() => {
164
+ if (value && adres.current)
165
+ birak();
166
+ }, [value]);
167
+ const kilit = disabled || mesgul !== null;
168
+ /* Dış değer değiştiyse sonuç bayat · ama çağıranın KENDİ güncellemesi değil:
169
+ onUpload içinde önbelleği sonuçla, onRemove içinde null ile güncelleyen bir
170
+ çağıran "yüklendi"yi yine duymalı. */
171
+ const bayat = (benim, baslangic, beklenen) => !canli.current ||
172
+ istek.current !== benim ||
173
+ (sonDeger.current !== baslangic && (beklenen === undefined || sonDeger.current !== beklenen));
174
+ function sifirla() {
175
+ /* GİRDİ SIFIRLANIYOR: aynı dosya ikinci kez seçilince `change` hiç
176
+ çıkmıyor, çünkü değer değişmiyor. Kullanıcı için bu "ikinci denemede
177
+ çalışmıyor" demek. */
178
+ if (girdi.current)
179
+ girdi.current.value = "";
180
+ }
125
181
  async function al(file) {
126
- if (!file)
182
+ if (!file || kilit)
183
+ return;
184
+ sifirla();
185
+ setHata(null);
186
+ setOnay(null);
187
+ if (!kabul(file, accept))
188
+ return setHata(labels.errorType);
189
+ if (file.size === 0)
190
+ return setHata(labels.errorUnreadable);
191
+ if (file.size > maxBytes)
192
+ return setHata(labels.errorSize);
193
+ if (!onUpload) {
194
+ try {
195
+ onChange(await prepareImage(file, maxEdge ?? Infinity));
196
+ }
197
+ catch (e) {
198
+ const kod = e instanceof Error ? e.message : "unreadable";
199
+ setHata(kod === "type" ? labels.errorType : kod === "size" ? labels.errorSize : labels.errorUnreadable);
200
+ }
201
+ return;
202
+ }
203
+ birak();
204
+ const url = URL.createObjectURL(file);
205
+ adres.current = url;
206
+ setBekleyen({ file, url });
207
+ if (!confirm)
208
+ await gonder(file);
209
+ }
210
+ async function gonder(file) {
211
+ if (!onUpload || mesgul)
212
+ return;
213
+ const benim = ++istek.current;
214
+ const baslangic = sonDeger.current;
215
+ setHata(null);
216
+ setOnay(null);
217
+ setMesgul("upload");
218
+ try {
219
+ const sonuc = await onUpload(file);
220
+ if (bayat(benim, baslangic, typeof sonuc === "string" ? sonuc : undefined))
221
+ return;
222
+ if (typeof sonuc === "string") {
223
+ birak();
224
+ onChange(sonuc);
225
+ setOnay(labels.uploaded ?? null);
226
+ }
227
+ else if (sonuc !== false) {
228
+ uyar("ImageField: onUpload resolved with neither an address nor `false`; nothing was announced. Return the stored address, or `false` when you handled the result yourself.");
229
+ }
230
+ }
231
+ catch (e) {
232
+ if (bayat(benim, baslangic, undefined))
233
+ return;
234
+ setHata(metin(e, labels));
235
+ }
236
+ finally {
237
+ if (canli.current)
238
+ setMesgul(null);
239
+ }
240
+ }
241
+ async function kaldir() {
242
+ if (kilit)
127
243
  return;
128
244
  setHata(null);
245
+ setOnay(null);
246
+ if (!onRemove)
247
+ return onChange(null);
248
+ const benim = ++istek.current;
249
+ const baslangic = sonDeger.current;
250
+ setMesgul("remove");
129
251
  try {
130
- onChange(await prepareImage(file, maxEdge));
252
+ const sonuc = await onRemove();
253
+ if (bayat(benim, baslangic, null) || sonuc === false)
254
+ return;
255
+ onChange(null);
256
+ setOnay(labels.removed ?? null);
131
257
  }
132
258
  catch (e) {
133
- const kod = e instanceof Error ? e.message : "unreadable";
134
- setHata(kod === "type" ? labels.errorType : kod === "size" ? labels.errorSize : labels.errorUnreadable);
259
+ if (bayat(benim, baslangic, undefined))
260
+ return;
261
+ setHata(metin(e, labels));
135
262
  }
136
263
  finally {
137
- /* GİRDİ SIFIRLANIYOR: aynı dosya ikinci kez seçilince `change` hiç
138
- çıkmıyor, çünkü değer değişmiyor. Kullanıcı için bu "ikinci denemede
139
- çalışmıyor" demek. */
140
- if (girdi.current)
141
- girdi.current.value = "";
264
+ if (canli.current)
265
+ setMesgul(null);
142
266
  }
143
267
  }
144
- return (_jsxs("div", { ...dataProps(rest), className: cn("flex flex-col gap-3", className), children: [_jsx("input", { ref: girdi, id: id, type: "file", accept: IMAGE_ACCEPT, className: "sr-only", onChange: (e) => al(e.target.files?.[0]) }), _jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx("span", { className: "tamga-image-field", "data-empty": value ? undefined : "true", children: value ? preview(value) : labels.empty }), _jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { type: "button", onClick: () => girdi.current?.click(), children: value ? labels.replace : labels.upload }), extra, value ? (_jsx(Button, { type: "button", variant: "ghost", onClick: () => onChange(null), children: labels.remove })) : null] })] }), hata ? _jsx(Alert, { state: "danger", title: hata }) : null] }));
268
+ const gosterilen = bekleyen?.url ?? value;
269
+ const asenkron = Boolean(onUpload || onRemove);
270
+ const notKimligi = `${id}-kilit`;
271
+ const tarif = disabled && lockedNote ? notKimligi : undefined;
272
+ return (_jsxs("div", { ...dataProps(rest), className: cn("flex flex-col gap-3", className), children: [_jsx("input", { ref: girdi, id: id, type: "file", accept: accept, className: "sr-only", disabled: kilit || undefined, onChange: (e) => al(e.target.files?.[0]) }), _jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx("span", { className: "tamga-image-field", "data-empty": gosterilen ? undefined : "true", children: gosterilen ? preview(gosterilen) : labels.empty }), _jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [bekleyen ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", busy: mesgul === "upload", busyLabel: labels.uploading ?? labels.upload, disabled: kilit || undefined, "aria-describedby": tarif, onClick: () => gonder(bekleyen.file), children: labels.confirm ?? labels.upload }), _jsx(Button, { type: "button", variant: "ghost", disabled: kilit || undefined, onClick: birak, children: labels.cancel ?? labels.remove })] })) : (_jsx(Button, { type: "button", busy: mesgul === "upload", busyLabel: labels.uploading ?? labels.upload, disabled: kilit || undefined, "aria-describedby": tarif, onClick: () => girdi.current?.click(), children: value ? labels.replace : labels.upload })), extra, value && !bekleyen ? (_jsx(Button, { type: "button", variant: "ghost", busy: mesgul === "remove", busyLabel: labels.removing ?? labels.remove, disabled: kilit || undefined, "aria-describedby": tarif, onClick: kaldir, children: labels.remove })) : null] })] }), lockedNote && disabled ? (_jsx("p", { id: notKimligi, className: "text-small text-ink-faint", children: lockedNote })) : null, asenkron ? (_jsx("p", { role: "status", className: cn("text-small text-ink-faint", !onay && "sr-only"), children: onay })) : null, asenkron ? (_jsx("div", { role: "alert", className: cn(!hata && "sr-only"), children: hata ? _jsx(Alert, { state: "danger", title: hata }) : null })) : hata ? (_jsx(Alert, { state: "danger", title: hata })) : null] }));
273
+ }
274
+ /** `accept` listesine karşı ön denetim: MIME, `image/*` ve `.png` biçimleri. Boş liste her şeyi geçirir. */
275
+ function kabul(file, accept) {
276
+ const liste = accept.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
277
+ if (liste.length === 0)
278
+ return true;
279
+ const tur = file.type.toLowerCase();
280
+ const ad = file.name.toLowerCase();
281
+ return liste.some((k) => k.startsWith(".") ? ad.endsWith(k) : k.endsWith("/*") ? tur.startsWith(k.slice(0, -1)) : tur === k);
282
+ }
283
+ /** Çağıranın reddi: metin, `Error` ya da `{ message }` olduğu gibi; boşsa kitin genel sözü. */
284
+ function metin(e, labels) {
285
+ const yedek = labels.errorUpload ?? labels.errorUnreadable;
286
+ if (typeof e === "string")
287
+ return e || yedek;
288
+ if (e && typeof e === "object" && "message" in e && typeof e.message === "string" && e.message)
289
+ return e.message;
290
+ return yedek;
145
291
  }