@morlay/dsh-client-ui-primitives 0.0.7 → 0.0.8

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 (67) hide show
  1. package/README.md +29 -5
  2. package/dist/client.d.mts +186 -11201
  3. package/dist/client.mjs +981 -1827
  4. package/package.json +5 -7
  5. package/src/client/controls/Bubble.module.css +17 -0
  6. package/src/client/controls/Bubble.tsx +17 -0
  7. package/src/client/controls/Button.module.css +10 -0
  8. package/src/client/controls/Button.tsx +29 -0
  9. package/src/client/controls/DisclosureRow.module.css +22 -0
  10. package/src/client/controls/DisclosureRow.tsx +20 -0
  11. package/src/client/controls/IconButton.module.css +50 -0
  12. package/src/client/controls/IconButton.tsx +45 -0
  13. package/src/client/controls/ModelRouteList.module.css +100 -0
  14. package/src/client/controls/ModelRouteList.tsx +118 -0
  15. package/src/client/controls/MultilineField.module.css +26 -0
  16. package/src/client/controls/MultilineField.tsx +19 -0
  17. package/src/client/controls/SearchInput.module.css +11 -0
  18. package/src/client/controls/SearchInput.tsx +22 -0
  19. package/src/client/controls/SearchSelect.module.css +34 -0
  20. package/src/client/controls/SearchSelect.tsx +138 -0
  21. package/src/client/controls/SeatButton.module.css +53 -0
  22. package/src/client/controls/SeatButton.tsx +40 -0
  23. package/src/client/controls/Spinner.module.css +18 -0
  24. package/src/client/controls/Spinner.tsx +14 -0
  25. package/src/client/controls/Tabs.module.css +48 -0
  26. package/src/client/controls/Tabs.tsx +57 -0
  27. package/src/client/controls/TagInput.module.css +105 -0
  28. package/src/client/controls/TagInput.tsx +152 -0
  29. package/src/client/controls/TimeLabel.module.css +16 -0
  30. package/src/client/controls/TimeLabel.tsx +15 -0
  31. package/src/client/controls/VisuallyHidden.module.css +9 -0
  32. package/src/client/controls/VisuallyHidden.tsx +16 -0
  33. package/src/client/controls/classes.ts +7 -0
  34. package/src/client/controls/index.ts +30 -0
  35. package/src/client/controls/tags.ts +24 -0
  36. package/src/client/index.ts +10 -2
  37. package/src/client/layout/Row.tsx +86 -0
  38. package/src/client/layout/Stack.tsx +79 -0
  39. package/src/client/layout/Text.tsx +54 -0
  40. package/src/client/layout/index.ts +16 -0
  41. package/src/client/layout/layout.module.css +217 -0
  42. package/src/client/layout/scales.ts +91 -0
  43. package/src/client/schema-form/Editor.tsx +48 -65
  44. package/src/client/schema-form/SchemaForm.tsx +4 -4
  45. package/src/client/schema-form/controller.ts +5 -2
  46. package/src/client/schema-form/geometry.ts +3 -0
  47. package/src/client/schema-form/schema-form.module.css +335 -0
  48. package/src/client/schema-form/value.tsx +10 -9
  49. package/src/client/settings-form/SettingsFieldRow.module.css +93 -0
  50. package/src/client/settings-form/SettingsFieldRow.tsx +103 -0
  51. package/src/client/settings-form/index.ts +5 -18
  52. package/src/css-modules.d.ts +6 -0
  53. package/src/client/schema-form/styles.ts +0 -321
  54. package/src/client/settings-form/SettingsForm.styles.ts +0 -58
  55. package/src/client/settings-form/SettingsForm.tsx +0 -68
  56. package/src/client/settings-form/fields.styles.ts +0 -158
  57. package/src/client/settings-form/fields.tsx +0 -184
  58. package/src/client/settings-form/form-model.ts +0 -339
  59. package/src/client/styling/css.ts +0 -27
  60. package/src/client/styling/focus.ts +0 -9
  61. package/src/client/styling/index.ts +0 -6
  62. package/src/client/styling/styled.tsx +0 -54
  63. package/src/client/styling/styling.ts +0 -142
  64. package/src/client/styling/token.ts +0 -275
  65. package/src/client/styling/toolkit.ts +0 -37
  66. package/src/client/theme.generated.ts +0 -846
  67. package/src/client/theme.ts +0 -5
@@ -1,339 +0,0 @@
1
- // 与 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/form-model.ts 逐行对应;
2
- // 改这里之前先看上游是否也改了同一处(上游只读)。
3
-
4
- // The staged form model behind a plugin's settings page: a card stages what the user types and writes it only on
5
- // save, and a field is marked overridden by its user-layer presence rather than by a value comparison.
6
-
7
- import { createSnapshotStore, type SnapshotStore } from "@deepseek-ai/dsh-client-store";
8
-
9
- // What the model reads of one Host entry's form.
10
- export interface SettingsFormScopeSnapshot<T> {
11
- // `ready` while the Host serves the entry to this client; the form renders nothing otherwise.
12
- status: "loading" | "ready" | "unavailable";
13
- // Last accepted schema-resolved section; undefined before the first acceptance.
14
- value: T | undefined;
15
- // Composition layer the value resolves over: what a field reverts to once cleared.
16
- base: unknown;
17
- // Raw user layer as stored; a field's PRESENCE here is what marks it overridden.
18
- user: unknown;
19
- // Whether the Host document accepts writes.
20
- writable: boolean;
21
- // Revision the snapshot was read at; a save fences its mutation with the revision its drafts started from.
22
- revision: number | undefined;
23
- }
24
-
25
- // One path edit a save sends, as the shared configuration form's `mutate` accepts it.
26
- export type SettingsFormPathOp =
27
- | { op: "set"; path: readonly string[]; value: unknown }
28
- | { op: "unset"; path: readonly string[] };
29
-
30
- // The entry form the model stages over: the reads and the atomic write of the form `ui-settings` shares per Host entry.
31
- export interface SettingsFormScope<T> {
32
- getSnapshot(): SettingsFormScopeSnapshot<T>;
33
- // Observe snapshot replacements.
34
- subscribe(listener: () => void): () => void;
35
- // Apply ordered field edits in one revision-fenced write; resolves after any recovery read.
36
- mutate(ops: readonly SettingsFormPathOp[], expectedRevision?: number): Promise<boolean>;
37
- }
38
-
39
- // The write one field's staged text performs when the card is saved.
40
- export type SettingsFieldWrite = { kind: "set"; value: unknown } | { kind: "clear" };
41
-
42
- // How one section field converts between its stored value and its draft text.
43
- export interface SettingsFieldSpec {
44
- // Field name inside the namespace section.
45
- field: string;
46
- // Render a stored value as draft text; the empty string when the section carries none.
47
- format: (value: unknown) => string;
48
- // The write this draft text stages, or undefined when the text is not a value this field accepts (blocks the save).
49
- parse: (text: string) => SettingsFieldWrite | undefined;
50
- }
51
-
52
- // A control whose value is written outside the settings section: a credential literal never rides a response, so its
53
- // draft is blank until typed and a blank draft writes nothing.
54
- export interface SettingsSecretSpec {
55
- // Field name addressing this control inside the card's form.
56
- field: string;
57
- // Write the staged text; resolves to whether the Host accepted it.
58
- write: (text: string) => Promise<boolean>;
59
- }
60
-
61
- // One field as a card's control renders it.
62
- export interface SettingsFieldState {
63
- // Draft text the control renders.
64
- text: string;
65
- // Whether saving would leave a user-layer entry for this field; a staged edit answers for itself.
66
- overridden: boolean;
67
- // Whether the draft is not a value this field accepts, which blocks saving.
68
- invalid: boolean;
69
- }
70
-
71
- // Form state every plugin card shares.
72
- export interface SettingsFormShell {
73
- // False while the namespace is not served to this client; the card renders nothing.
74
- available: boolean;
75
- // Whether the Host document accepts writes.
76
- writable: boolean;
77
- // Whether the form holds edits that a save would write.
78
- dirty: boolean;
79
- // Whether any staged draft is invalid, which blocks the save.
80
- invalid: boolean;
81
- // Whether a save is crossing the wire.
82
- saving: boolean;
83
- // Whether the last save did not land as staged; cleared by the next edit or save.
84
- failed: boolean;
85
- }
86
-
87
- // The write actions every plugin card's slot entry injects.
88
- export interface SettingsFormActions {
89
- // Stage draft text for one field.
90
- edit: (field: string, text: string) => void;
91
- // Stage a clear, so saving lets the field re-inherit the composition layer.
92
- resetField: (field: string) => void;
93
- // Write every staged edit, then re-seed from what the Host accepted.
94
- save: () => void;
95
- // Drop every staged edit.
96
- discard: () => void;
97
- }
98
-
99
- // One field's staged edit.
100
- interface StagedEdit {
101
- // Draft text the control renders.
102
- text: string;
103
- // True when this edit clears the field whatever text it shows.
104
- clear: boolean;
105
- }
106
-
107
- // One staged edit resolved into the write a save performs.
108
- interface PlannedWrite {
109
- // Field this entry writes.
110
- field: string;
111
- // Perform the write and report whether the Host holds the staged value afterwards.
112
- run?: () => Promise<boolean>;
113
- op?: SettingsFormPathOp;
114
- }
115
-
116
- // A whole-number field: an empty draft clears the field, any other draft that is not a finite number blocks the save.
117
- export function settingsNumberField(field: string): SettingsFieldSpec {
118
- return {
119
- field,
120
- // A section that carries no number for this field renders empty rather than as a value nobody chose.
121
- format: (value) => (typeof value === "number" ? String(value) : ""),
122
- parse: (text) => {
123
- const trimmed = text.trim();
124
- if (trimmed === "") return { kind: "clear" };
125
- const parsed = Number(trimmed);
126
- return Number.isFinite(parsed) ? { kind: "set", value: parsed } : undefined;
127
- },
128
- };
129
- }
130
-
131
- // A free-text field: an empty draft clears the field, so emptying the control and saving is the same as resetting it.
132
- export function settingsTextField(field: string): SettingsFieldSpec {
133
- return {
134
- field,
135
- format: (value) => (typeof value === "string" ? value : ""),
136
- parse: (text) => {
137
- const trimmed = text.trim();
138
- return trimmed === "" ? { kind: "clear" } : { kind: "set", value: trimmed };
139
- },
140
- };
141
- }
142
-
143
- // Stages one card's edits over one settings namespace and writes them on save. The form publishes through a snapshot
144
- // store because slot components read through a snapshot selector while both the scope and the drafts change underneath.
145
- export class SettingsFormModel<T> {
146
- private readonly specs: Map<string, SettingsFieldSpec>;
147
- private readonly secretSpecs: Map<string, SettingsSecretSpec>;
148
- private readonly staged = new Map<string, StagedEdit>();
149
- private readonly listeners = new Set<() => void>();
150
- private baseline: SettingsFormScopeSnapshot<T> | undefined;
151
- private readonly unsubscribe: () => void;
152
- private saving = false;
153
- private failed = false;
154
-
155
- constructor(
156
- private readonly scope: SettingsFormScope<T>,
157
- specs: SettingsFieldSpec[],
158
- secrets: SettingsSecretSpec[] = [],
159
- ) {
160
- this.specs = new Map(specs.map((spec) => [spec.field, spec]));
161
- this.secretSpecs = new Map(secrets.map((spec) => [spec.field, spec]));
162
- this.unsubscribe = scope.subscribe(() => {
163
- this.publish();
164
- });
165
- }
166
-
167
- // Publish a projection of this form, rebuilt whenever the scope or a draft changes.
168
- bind<S>(project: () => S): SnapshotStore<S> {
169
- const store = createSnapshotStore(project());
170
- this.listeners.add(() => {
171
- store.set(project());
172
- });
173
- return store;
174
- }
175
-
176
- // Read the card-level state: what the Host serves, and what a save would do.
177
- shell(): SettingsFormShell {
178
- const snapshot = this.scope.getSnapshot();
179
- const plan = this.plan();
180
- return {
181
- available: snapshot.status === "ready",
182
- writable: snapshot.writable,
183
- dirty: plan.length > 0,
184
- invalid: plan.some((item) => item.run === undefined && item.op === undefined),
185
- saving: this.saving,
186
- failed: this.failed,
187
- };
188
- }
189
-
190
- // Read one control's state: draft text, whether a save would leave an override, and whether it is invalid.
191
- field(field: string): SettingsFieldState {
192
- const staged = this.staged.get(field);
193
- if (this.secretSpecs.has(field)) {
194
- return { text: staged?.text ?? "", overridden: false, invalid: false };
195
- }
196
- const spec = this.spec(field);
197
- if (staged === undefined) {
198
- return {
199
- text: spec.format(this.sectionValue(field)),
200
- overridden: this.stored(field),
201
- invalid: false,
202
- };
203
- }
204
- const write = staged.clear ? { kind: "clear" as const } : spec.parse(staged.text);
205
- return {
206
- text: staged.text,
207
- overridden: write?.kind === "set",
208
- invalid: write === undefined,
209
- };
210
- }
211
-
212
- // Build the edit, reset, save, and discard actions bound to this form.
213
- actions(): SettingsFormActions {
214
- return {
215
- edit: (field, text) => {
216
- this.stage(field, { text, clear: false });
217
- },
218
- resetField: (field) => {
219
- this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true });
220
- },
221
- save: () => {
222
- void this.save();
223
- },
224
- discard: () => {
225
- if (this.staged.size === 0 && !this.failed) return;
226
- this.staged.clear();
227
- this.baseline = undefined;
228
- this.failed = false;
229
- this.publish();
230
- },
231
- };
232
- }
233
-
234
- // Write every staged edit, then re-seed from what the Host accepted: the outcome is read back rather than predicted
235
- // (the Host is the only authority), and a save that did not land keeps its drafts so the user can correct them.
236
- async save(): Promise<void> {
237
- const plan = this.plan();
238
- if (
239
- !plan.length ||
240
- this.saving ||
241
- !this.scope.getSnapshot().writable ||
242
- plan.some((item) => item.run === undefined && item.op === undefined)
243
- )
244
- return;
245
- this.saving = true;
246
- this.failed = false;
247
- this.publish();
248
- try {
249
- const ops = plan.flatMap((item) => (item.op === undefined ? [] : [item.op]));
250
- let landed = !ops.length || (await this.scope.mutate(ops, this.baseline?.revision));
251
- if (!landed) {
252
- this.failed = true;
253
- return;
254
- }
255
- for (const item of plan) if (item.run) landed = (await item.run()) && landed;
256
- if (landed) {
257
- this.staged.clear();
258
- this.baseline = undefined;
259
- }
260
- this.failed = !landed;
261
- } catch {
262
- this.failed = true;
263
- } finally {
264
- this.saving = false;
265
- this.publish();
266
- }
267
- }
268
-
269
- // Release the form's accepted-value subscription.
270
- dispose(): void {
271
- this.unsubscribe();
272
- this.listeners.clear();
273
- }
274
-
275
- // Every staged edit a save would write; an entry whose draft its field rejects carries no write (the form stays
276
- // dirty, and the save refuses rather than dropping the edit).
277
- private plan(): PlannedWrite[] {
278
- const plan: PlannedWrite[] = [];
279
- for (const [field, staged] of this.staged) {
280
- const secret = this.secretSpecs.get(field);
281
- if (secret !== undefined) {
282
- const value = staged.text.trim();
283
- if (value !== "") plan.push({ field, run: () => secret.write(value) });
284
- continue;
285
- }
286
- const spec = this.spec(field);
287
- if (staged.clear) {
288
- if (this.stored(field)) plan.push({ field, op: { op: "unset", path: [field] } });
289
- continue;
290
- }
291
- if (staged.text === spec.format(this.sectionValue(field))) continue;
292
- const write = spec.parse(staged.text);
293
- if (write === undefined) plan.push({ field });
294
- else if (write.kind === "clear") plan.push({ field, op: { op: "unset", path: [field] } });
295
- else plan.push({ field, op: { op: "set", path: [field], value: write.value } });
296
- }
297
- return plan;
298
- }
299
-
300
- private stage(field: string, edit: StagedEdit): void {
301
- this.baseline ??= this.scope.getSnapshot();
302
- this.staged.set(field, edit);
303
- this.failed = false;
304
- this.publish();
305
- }
306
-
307
- private spec(field: string): SettingsFieldSpec {
308
- const spec = this.specs.get(field);
309
- // Every call site names a field this card declared; a missing one is a wiring mistake that must not become a
310
- // silently inert control.
311
- if (spec === undefined) throw new Error(`plugin card has no field ${field}`);
312
- return spec;
313
- }
314
-
315
- private snapshotOf(): SettingsFormScopeSnapshot<T> {
316
- return this.scope.getSnapshot();
317
- }
318
-
319
- private sectionValue(field: string): unknown {
320
- return (this.snapshotOf().value as Record<string, unknown> | undefined)?.[field];
321
- }
322
-
323
- private baseValue(field: string): unknown {
324
- return (this.snapshotOf().base as Record<string, unknown> | undefined)?.[field];
325
- }
326
-
327
- private userLayer(): Record<string, unknown> | undefined {
328
- return this.snapshotOf().user as Record<string, unknown> | undefined;
329
- }
330
-
331
- private stored(field: string): boolean {
332
- const user = this.userLayer();
333
- return user !== undefined && Object.hasOwn(user, field);
334
- }
335
-
336
- private publish(): void {
337
- for (const listener of this.listeners) listener();
338
- }
339
- }
@@ -1,27 +0,0 @@
1
- import type { StandardProperties, VendorProperties } from "csstype";
2
-
3
- type NonStandardProperties = {
4
- cornerShape?: CSSValue<string>;
5
- clip?: CSSValue<string>;
6
- WebkitBoxOrient?: CSSValue<string>;
7
- };
8
-
9
- type Properties = Omit<StandardProperties & VendorProperties, keyof NonStandardProperties> &
10
- NonStandardProperties;
11
-
12
- export type CSSVar = `--${string}`;
13
- export type CSSSelector = `&${string}` | `${string}&` | `@${string}`;
14
-
15
- export type CSSValue<Fallback = string | number> = Fallback | (() => Iterable<Fallback>);
16
-
17
- export type CSSObject<Props extends object, Fallback = string | number> = {
18
- [K in keyof Props]?: Props[K] | CSSValue<Fallback>;
19
- } & {
20
- [K in CSSVar]?: CSSValue<Fallback>;
21
- };
22
-
23
- type Nested<Props extends object> = CSSObject<Props> & {
24
- [K in CSSSelector]?: Nested<Props>;
25
- };
26
-
27
- export type CSSProps = Nested<Properties>;
@@ -1,9 +0,0 @@
1
- // 焦点环口径搬上游 ui-theme 的 `focus.css`:颜色读官方 `--dsw-focus-ring-color`
2
- // (主题把它按输入模态解析成蓝,指针模态下置 transparent 抑制环),变量缺失时回退业务蓝;
3
- // 宽度读官方的 `--dsw-focus-ring-width`。偏移与线宽的特例仍由各控件自己声明。
4
- import { dsw } from "../theme.ts";
5
- import { Token } from "./token.ts";
6
-
7
- export const focusRing = `${String(dsw.focus.ring.width)} solid ${Token.collect(
8
- Token.fallbackVar(dsw.focus.ring.color, String(dsw.alias.state.business.primary)),
9
- )}`;
@@ -1,6 +0,0 @@
1
- export type * from "./css.ts";
2
- export { Styling, styling } from "./styling.ts";
3
- export { Token } from "./token.ts";
4
- export type { CSSVarRef, TokenVars, Tokens } from "./token.ts";
5
- export { styled, styleOf } from "./styled.tsx";
6
- export type { StyledComponent } from "./styled.tsx";
@@ -1,54 +0,0 @@
1
- // 轻量 styled:intrinsic / 组件 + ref,样式走 data-css 属性(不做 polymorphic,
2
- // 参考实现里的 asChild 由 ark-ui 提供,我们不需要)。
3
-
4
- import { createElement, forwardRef, useMemo } from "react";
5
- import type {
6
- ComponentProps,
7
- ComponentPropsWithRef,
8
- ElementType,
9
- ForwardRefExoticComponent,
10
- Ref,
11
- RefAttributes,
12
- } from "react";
13
- import type { CSSProps } from "./css.ts";
14
- import { styling } from "./styling.ts";
15
- import { toMerged } from "./toolkit.ts";
16
-
17
- type StyledProps<T extends ElementType> = Omit<ComponentProps<T>, "ref">;
18
- type StyledRef<T extends ElementType> =
19
- ComponentPropsWithRef<T>["ref"] extends Ref<infer R> ? R : never;
20
-
21
- const sxSymbol: unique symbol = Symbol("styled.sx");
22
-
23
- export type StyledComponent<T extends ElementType> = ForwardRefExoticComponent<
24
- StyledProps<T> & RefAttributes<StyledRef<T>>
25
- > & { readonly [sxSymbol]: CSSProps };
26
-
27
- // `styled('div')({ padding: '4px' }, { '&:hover': { … } })` → 带 `data-css-*` 的组件。
28
- // 多个样式对象按顺序深合并(后者覆盖前者),便于把变体叠在基样式上。
29
- export function styled<T extends ElementType>(
30
- Component: T,
31
- defaultProps: Partial<ComponentProps<T>> = {},
32
- ) {
33
- return (...styles: CSSProps[]): StyledComponent<T> => {
34
- const sx = styles.reduce<CSSProps>((merged, style) => toMerged(merged, style), {});
35
-
36
- const Styled = forwardRef<StyledRef<T>, StyledProps<T>>((props, ref) => {
37
- const styleProps = useMemo(() => styling.props(sx), []);
38
- return createElement(Component as ElementType, {
39
- ...defaultProps,
40
- ...props,
41
- ...styleProps,
42
- ref,
43
- });
44
- });
45
- Styled.displayName = `styled(${typeof Component === "string" ? Component : (Component.displayName ?? Component.name ?? "Component")})`;
46
-
47
- return Object.assign(Styled, { [sxSymbol]: sx }) as StyledComponent<T>;
48
- };
49
- }
50
-
51
- // 取回 styled 组件携带的样式对象(需要在其之上继续组合时用)。
52
- export function styleOf(component: StyledComponent<ElementType>): CSSProps {
53
- return component[sxSymbol];
54
- }
@@ -1,142 +0,0 @@
1
- import type { CSSProps } from "./css.ts";
2
- import { Token } from "./token.ts";
3
- import { isNull, isPlainObject, isUndefined, toMerged } from "./toolkit.ts";
4
-
5
- enum RuleType {
6
- global,
7
- keyframes,
8
- scoped,
9
- scopedClass,
10
- }
11
-
12
- interface Sheet {
13
- id: string;
14
- contents: string[];
15
- selector: string | undefined;
16
- }
17
-
18
- export class Styling {
19
- static create(): Styling {
20
- return new Styling();
21
- }
22
-
23
- static toRule(sheet: Sheet): string {
24
- return sheet.selector === undefined
25
- ? sheet.contents.join("")
26
- : `${sheet.selector} {${sheet.contents.join("")} }`;
27
- }
28
-
29
- #token = new Token();
30
- #sheets = new Map<string, Sheet>();
31
- #injected = new Set<string>();
32
-
33
- props = (...styles: (CSSProps | false | null | undefined)[]): Record<string, string> => {
34
- const merged = mergeStyles(styles);
35
- if (Object.keys(merged).length === 0) return {};
36
- const sheet = this.#sheet(RuleType.scoped, this.#rules(merged));
37
- return sheet === undefined ? {} : { [`data-css-${sheet.id}`]: "" };
38
- };
39
-
40
- keyframes = (frames: Record<string, CSSProps> = {}): string => {
41
- const sheet = this.#sheet(RuleType.keyframes, this.#rules(frames as unknown as CSSProps));
42
- return sheet?.id ?? "anim-none";
43
- };
44
-
45
- injectGlobals = (styles: Record<string, CSSProps> = {}): void => {
46
- for (const [name, style] of Object.entries(styles)) {
47
- this.#sheet(RuleType.global, this.#rules(style), name);
48
- }
49
- };
50
-
51
- className = (...styles: (CSSProps | false | null | undefined)[]): string => {
52
- const merged = mergeStyles(styles);
53
- if (Object.keys(merged).length === 0) return "";
54
- const sheet = this.#sheet(RuleType.scopedClass, this.#rules(merged));
55
- return sheet === undefined ? "" : `cls-${sheet.id}`;
56
- };
57
-
58
- sheets = (): string[] => [...this.#sheets.values()].map((sheet) => Styling.toRule(sheet));
59
-
60
- #sheet(type: RuleType, rules: Generator<string>, selector?: string): Sheet | undefined {
61
- let hash = 0;
62
- const contents: string[] = [];
63
- for (const rule of rules) {
64
- contents.push(rule);
65
- hash = incrementalHash(rule, hash);
66
- }
67
- if (selector !== undefined) hash = incrementalHash(selector, hash);
68
- if (hash === 0) return undefined;
69
-
70
- let id = selector ?? "";
71
- if (type === RuleType.scoped) id = `s${hash.toString(36)}`;
72
- if (type === RuleType.scopedClass) id = `c${hash.toString(36)}`;
73
- if (type === RuleType.global) id = `g${hash.toString(36)}`;
74
- if (type === RuleType.keyframes) id = `a${hash.toString(36)}`;
75
-
76
- let resolvedSelector = selector;
77
- if (resolvedSelector === undefined) {
78
- if (type === RuleType.keyframes) resolvedSelector = `@keyframes ${id}`;
79
- if (type === RuleType.scoped) resolvedSelector = `[data-css-${id}]`;
80
- if (type === RuleType.scopedClass) resolvedSelector = `.cls-${id}`;
81
- }
82
-
83
- const sheet: Sheet = { id, contents, selector: resolvedSelector };
84
- const existing = this.#sheets.get(id);
85
- if (existing !== undefined && existing.contents.join("") === contents.join("")) return existing;
86
- this.#sheets.set(id, sheet);
87
- this.#inject(sheet);
88
- return sheet;
89
- }
90
-
91
- #inject(sheet: Sheet): void {
92
- if (this.#injected.has(sheet.id)) return;
93
- this.#injected.add(sheet.id);
94
- if (typeof document === "undefined") return;
95
- const element = document.createElement("style");
96
- element.setAttribute("data-css", sheet.id);
97
- element.textContent = Styling.toRule(sheet);
98
- document.head.appendChild(element);
99
- }
100
-
101
- *#rules(styles: CSSProps): Generator<string> {
102
- const nested = new Map<string, CSSProps>();
103
-
104
- for (const [key, value] of Object.entries(styles)) {
105
- if (isNull(value) || isUndefined(value)) continue;
106
- if (isPlainObject(value)) {
107
- nested.set(key, value as unknown as CSSProps);
108
- continue;
109
- }
110
- yield ` ${this.#token.prop(key)}: ${this.#token.normalize(value)};`;
111
- }
112
-
113
- for (const [key, value] of nested.entries()) {
114
- yield ` ${key} {`;
115
- yield* this.#rules(value);
116
- yield " }";
117
- }
118
- }
119
- }
120
-
121
- function mergeStyles(styles: readonly (CSSProps | false | null | undefined)[]): CSSProps {
122
- return styles.reduce<CSSProps>(
123
- (accumulated, style) => (style ? (toMerged(accumulated, style) as CSSProps) : accumulated),
124
- {},
125
- );
126
- }
127
-
128
- function incrementalHash(value: string, previous = 0): number {
129
- let hash = previous;
130
- for (let index = 0; index < value.length; index += 1) {
131
- hash = (hash << 5) - hash + value.charCodeAt(index);
132
- hash &= 0x7fffffff;
133
- }
134
- return hash;
135
- }
136
-
137
- // 全局单例:本包被 inline 进各消费方的产物后,同一个页面会有多份模块实例,而样式表与注入状态必须只有一份
138
- // ——否则同一份规则按各自的实例各注入一遍、同名的 global 规则互相顶替。`Styling.create()` 仍是独立实例工厂。
139
- const STYLING_SINGLETON = Symbol.for("@morlay/dsh-client-ui-primitives/styling");
140
- const globalScope = globalThis as unknown as { [key: symbol]: unknown };
141
-
142
- export const styling: Styling = (globalScope[STYLING_SINGLETON] ??= Styling.create()) as Styling;