@qti-components/base 1.5.1 → 2.0.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.
- package/dist/abstract/interaction.d.ts +54 -64
- package/dist/abstract/interaction.js +103 -276
- package/dist/abstract/interaction.js.map +1 -1
- package/dist/abstract/qti-expression.d.ts +1 -1
- package/dist/abstract/qti-expression.js +23 -2
- package/dist/abstract/qti-expression.js.map +1 -1
- package/dist/abstract/qti-feedback.js +1 -1
- package/dist/abstract/qti-feedback.js.map +1 -1
- package/dist/context/computed-item.context.d.ts +1 -16
- package/dist/context/computed-item.context.js.map +1 -1
- package/dist/context/computed.context.d.ts +1 -26
- package/dist/context/computed.context.js.map +1 -1
- package/dist/context/config.context.d.ts +61 -12
- package/dist/context/config.context.js.map +1 -1
- package/dist/context/interaction.context.d.ts +36 -0
- package/dist/context/interaction.context.js +3 -0
- package/dist/context/interaction.context.js.map +1 -0
- package/dist/context/item.context.d.ts +4 -12
- package/dist/context/item.context.js +2 -16
- package/dist/context/item.context.js.map +1 -1
- package/dist/context/test.context.d.ts +1 -1
- package/dist/context/test.context.js.map +1 -1
- package/dist/context/types/computed-item.types.d.ts +16 -0
- package/dist/context/types/computed-item.types.js +2 -0
- package/dist/context/types/computed-item.types.js.map +1 -0
- package/dist/context/types/computed.types.d.ts +26 -0
- package/dist/context/types/computed.types.js +2 -0
- package/dist/context/types/computed.types.js.map +1 -0
- package/dist/context/types/item.types.d.ts +12 -0
- package/dist/context/types/item.types.js +17 -0
- package/dist/context/types/item.types.js.map +1 -0
- package/dist/elements.d.ts +24 -0
- package/dist/elements.js +22 -0
- package/dist/elements.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/interaction.interface.d.ts +39 -15
- package/dist/lib/interaction.interface.js.map +1 -1
- package/dist/lib/response.d.ts +104 -0
- package/dist/lib/response.js +168 -0
- package/dist/lib/response.js.map +1 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +7 -0
- package/dist/register.js.map +1 -0
- package/dist/styles/box-sizing.styles.d.ts +19 -0
- package/dist/styles/box-sizing.styles.js +31 -0
- package/dist/styles/box-sizing.styles.js.map +1 -0
- package/dist/styles/drop-region.styles.d.ts +36 -0
- package/dist/styles/drop-region.styles.js +78 -0
- package/dist/styles/drop-region.styles.js.map +1 -0
- package/dist/styles/validation-message.styles.d.ts +18 -0
- package/dist/styles/validation-message.styles.js +24 -0
- package/dist/styles/validation-message.styles.js.map +1 -0
- package/package.json +16 -1
- package/dist/context/qti-assessment-item.context.d.ts +0 -4
- package/dist/context/qti-assessment-item.context.js +0 -3
- package/dist/context/qti-assessment-item.context.js.map +0 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { VariableDeclaration } from '../../lib/variables';
|
|
2
|
+
export interface ItemContext {
|
|
3
|
+
identifier?: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
variables?: ReadonlyArray<VariableDeclaration<string | string[] | null>>;
|
|
6
|
+
/**
|
|
7
|
+
* Optional per-interaction opaque state, keyed by responseIdentifier.
|
|
8
|
+
* Used for interactions that support state save/restore (e.g. PCI).
|
|
9
|
+
*/
|
|
10
|
+
state?: Record<string, string | null>;
|
|
11
|
+
}
|
|
12
|
+
export declare const itemContextVariables: VariableDeclaration<string | string[]>[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const itemContextVariables = [
|
|
2
|
+
{
|
|
3
|
+
identifier: 'completionStatus',
|
|
4
|
+
cardinality: 'single',
|
|
5
|
+
baseType: 'string',
|
|
6
|
+
value: 'unknown',
|
|
7
|
+
type: 'outcome'
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
identifier: 'numAttempts',
|
|
11
|
+
cardinality: 'single',
|
|
12
|
+
baseType: 'integer',
|
|
13
|
+
value: '0',
|
|
14
|
+
type: 'response'
|
|
15
|
+
}
|
|
16
|
+
];
|
|
17
|
+
//# sourceMappingURL=item.types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"item.types.js","sourceRoot":"","sources":["../../../src/context/types/item.types.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC;QACE,UAAU,EAAE,kBAAkB;QAC9B,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,SAAS;KAChB;IACD;QACE,UAAU,EAAE,aAAa;QACzB,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,SAAS;QACnB,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,UAAU;KACjB;CAC0C,CAAC","sourcesContent":["import type { VariableDeclaration } from '../../lib/variables';\n\nexport interface ItemContext {\n identifier?: string;\n href?: string;\n variables?: ReadonlyArray<VariableDeclaration<string | string[] | null>>;\n /**\n * Optional per-interaction opaque state, keyed by responseIdentifier.\n * Used for interactions that support state save/restore (e.g. PCI).\n */\n state?: Record<string, string | null>;\n}\n\nexport const itemContextVariables = [\n {\n identifier: 'completionStatus',\n cardinality: 'single',\n baseType: 'string',\n value: 'unknown',\n type: 'outcome'\n },\n {\n identifier: 'numAttempts',\n cardinality: 'single',\n baseType: 'integer',\n value: '0',\n type: 'response'\n }\n] as VariableDeclaration<string | string[]>[];\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom elements this package registers into an application's registry.
|
|
3
|
+
*
|
|
4
|
+
* Empty, deliberately. `qti-base` is abstractions and context — it contributes no elements to a
|
|
5
|
+
* delivery application.
|
|
6
|
+
*
|
|
7
|
+
* The one entry that used to be here, `qti-config-test-provider`, is gone entirely. It was a
|
|
8
|
+
* TEST-ONLY wrapper that every consumer of `import '@qti-components/base'` registered transitively,
|
|
9
|
+
* and it carried `@customElement(...)` on top of this list — an UNGUARDED `customElements.define()`
|
|
10
|
+
* at module-evaluation time. A module graph holding two copies of this package (a workspace build
|
|
11
|
+
* alongside a published dist, which is what the editor resolves) evaluated it twice and the second
|
|
12
|
+
* threw `NotSupportedError: ... already been used with this registry`, aborting module evaluation
|
|
13
|
+
* and taking down every test file that imported it — 34 suites in the editor, none of which used
|
|
14
|
+
* the element. `register.ts` had the guard right all along; the decorator bypassed it.
|
|
15
|
+
*
|
|
16
|
+
* Config for a single interaction is a property binding now — see config.context.ts.
|
|
17
|
+
*
|
|
18
|
+
* The array and `register.ts` stay so the elements + guarded-register convention the other 30
|
|
19
|
+
* packages follow is already in place the day this package does own an element.
|
|
20
|
+
*/
|
|
21
|
+
export declare const qtiBaseElements: readonly {
|
|
22
|
+
tag: string;
|
|
23
|
+
ctor: CustomElementConstructor;
|
|
24
|
+
}[];
|
package/dist/elements.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom elements this package registers into an application's registry.
|
|
3
|
+
*
|
|
4
|
+
* Empty, deliberately. `qti-base` is abstractions and context — it contributes no elements to a
|
|
5
|
+
* delivery application.
|
|
6
|
+
*
|
|
7
|
+
* The one entry that used to be here, `qti-config-test-provider`, is gone entirely. It was a
|
|
8
|
+
* TEST-ONLY wrapper that every consumer of `import '@qti-components/base'` registered transitively,
|
|
9
|
+
* and it carried `@customElement(...)` on top of this list — an UNGUARDED `customElements.define()`
|
|
10
|
+
* at module-evaluation time. A module graph holding two copies of this package (a workspace build
|
|
11
|
+
* alongside a published dist, which is what the editor resolves) evaluated it twice and the second
|
|
12
|
+
* threw `NotSupportedError: ... already been used with this registry`, aborting module evaluation
|
|
13
|
+
* and taking down every test file that imported it — 34 suites in the editor, none of which used
|
|
14
|
+
* the element. `register.ts` had the guard right all along; the decorator bypassed it.
|
|
15
|
+
*
|
|
16
|
+
* Config for a single interaction is a property binding now — see config.context.ts.
|
|
17
|
+
*
|
|
18
|
+
* The array and `register.ts` stay so the elements + guarded-register convention the other 30
|
|
19
|
+
* packages follow is already in place the day this package does own an element.
|
|
20
|
+
*/
|
|
21
|
+
export const qtiBaseElements = [];
|
|
22
|
+
//# sourceMappingURL=elements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,eAAe,GAA+D,EAAE,CAAC","sourcesContent":["/**\n * Custom elements this package registers into an application's registry.\n *\n * Empty, deliberately. `qti-base` is abstractions and context — it contributes no elements to a\n * delivery application.\n *\n * The one entry that used to be here, `qti-config-test-provider`, is gone entirely. It was a\n * TEST-ONLY wrapper that every consumer of `import '@qti-components/base'` registered transitively,\n * and it carried `@customElement(...)` on top of this list — an UNGUARDED `customElements.define()`\n * at module-evaluation time. A module graph holding two copies of this package (a workspace build\n * alongside a published dist, which is what the editor resolves) evaluated it twice and the second\n * threw `NotSupportedError: ... already been used with this registry`, aborting module evaluation\n * and taking down every test file that imported it — 34 suites in the editor, none of which used\n * the element. `register.ts` had the guard right all along; the decorator bypassed it.\n *\n * Config for a single interaction is a property binding now — see config.context.ts.\n *\n * The array and `register.ts` stay so the elements + guarded-register convention the other 30\n * packages follow is already in place the day this package does own an element.\n */\nexport const qtiBaseElements: readonly { tag: string; ctor: CustomElementConstructor }[] = [];\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
import './register';
|
|
1
2
|
export * from './context/computed-item.context';
|
|
3
|
+
export * from './context/types/computed-item.types';
|
|
2
4
|
export * from './context/computed.context';
|
|
5
|
+
export * from './context/types/computed.types';
|
|
3
6
|
export * from './context/config.context';
|
|
7
|
+
export * from './context/interaction.context';
|
|
4
8
|
export * from './context/item.context';
|
|
5
|
-
export * from './context/
|
|
9
|
+
export * from './context/types/item.types';
|
|
6
10
|
export * from './context/qti.context';
|
|
7
11
|
export * from './context/session.context';
|
|
8
12
|
export * from './context/test.context';
|
|
13
|
+
export * from './styles/box-sizing.styles';
|
|
14
|
+
export * from './styles/drop-region.styles';
|
|
15
|
+
export * from './styles/validation-message.styles';
|
|
9
16
|
export * from './abstract/interaction';
|
|
10
17
|
export * from './abstract/qti-variable-declaration';
|
|
11
18
|
export * from './abstract/qti-condition-expression';
|
|
12
19
|
export * from './abstract/qti-feedback';
|
|
13
20
|
export * from './abstract/qti-expression';
|
|
21
|
+
export * from './lib/response';
|
|
14
22
|
export * from './lib/expression-result';
|
|
15
23
|
export * from './lib/interaction.interface';
|
|
16
24
|
export * from './lib/variables';
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
import './register';
|
|
1
2
|
export * from './context/computed-item.context';
|
|
3
|
+
export * from './context/types/computed-item.types';
|
|
2
4
|
export * from './context/computed.context';
|
|
5
|
+
export * from './context/types/computed.types';
|
|
3
6
|
export * from './context/config.context';
|
|
7
|
+
export * from './context/interaction.context';
|
|
4
8
|
export * from './context/item.context';
|
|
5
|
-
export * from './context/
|
|
9
|
+
export * from './context/types/item.types';
|
|
6
10
|
export * from './context/qti.context';
|
|
7
11
|
export * from './context/session.context';
|
|
8
12
|
export * from './context/test.context';
|
|
13
|
+
export * from './styles/box-sizing.styles';
|
|
14
|
+
export * from './styles/drop-region.styles';
|
|
15
|
+
export * from './styles/validation-message.styles';
|
|
9
16
|
export * from './abstract/interaction';
|
|
10
17
|
export * from './abstract/qti-variable-declaration';
|
|
11
18
|
export * from './abstract/qti-condition-expression';
|
|
12
19
|
export * from './abstract/qti-feedback';
|
|
13
20
|
export * from './abstract/qti-expression';
|
|
21
|
+
export * from './lib/response';
|
|
14
22
|
export * from './lib/expression-result';
|
|
15
23
|
export * from './lib/interaction.interface';
|
|
16
24
|
export * from './lib/variables';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/qti-rule-base';\n\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
|
|
@@ -1,20 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
showCandidateCorrection: boolean;
|
|
10
|
-
response: string | string[];
|
|
11
|
-
responseIdentifier: string;
|
|
1
|
+
/**
|
|
2
|
+
* Smallest shared contract for any form-associated custom element.
|
|
3
|
+
*
|
|
4
|
+
* Keep this intentionally tiny and platform-aligned. QTI-specific concerns
|
|
5
|
+
* such as response identifiers, readonly handling, and validation
|
|
6
|
+
* should be layered via separate capability interfaces below.
|
|
7
|
+
*/
|
|
8
|
+
export interface FormAssociatedInteraction extends HTMLElement {
|
|
12
9
|
disabled: boolean;
|
|
10
|
+
readonly internals: ElementInternals;
|
|
11
|
+
formResetCallback(): void;
|
|
12
|
+
}
|
|
13
|
+
/** QTI response identity capability. */
|
|
14
|
+
export interface ResponseIdentifiedInteraction {
|
|
15
|
+
responseIdentifier: string;
|
|
16
|
+
}
|
|
17
|
+
/** Optional readonly capability used by many QTI interactions. */
|
|
18
|
+
export interface ReadonlyInteraction {
|
|
13
19
|
readonly: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Optional reset capability beyond the platform form reset callback. */
|
|
22
|
+
export interface ResettableInteraction {
|
|
23
|
+
reset(): void;
|
|
24
|
+
}
|
|
25
|
+
/** Interaction that publishes and stores a response value. */
|
|
26
|
+
export interface ResponseInteractionElement extends FormAssociatedInteraction {
|
|
27
|
+
responseIdentifier: string;
|
|
28
|
+
response: string | string[] | null;
|
|
29
|
+
saveResponse(value: string | string[], state?: string | null): void;
|
|
30
|
+
}
|
|
31
|
+
/** Interaction that participates in validity reporting. */
|
|
32
|
+
export interface ValidatableInteraction extends FormAssociatedInteraction {
|
|
14
33
|
validate(): boolean;
|
|
15
34
|
reportValidity(): boolean;
|
|
16
|
-
reset(): void;
|
|
17
|
-
saveResponse(value: string | string[]): void;
|
|
18
|
-
toggleCorrectResponse(show: boolean): void;
|
|
19
|
-
toggleCandidateCorrection(show: boolean): void;
|
|
20
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Common contract expected by item containers when tracking registered interactions.
|
|
38
|
+
*/
|
|
39
|
+
export interface RegisteredInteraction extends ResponseInteractionElement, ValidatableInteraction, ReadonlyInteraction, ResettableInteraction {
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Prefer `RegisteredInteraction` for new code.
|
|
43
|
+
*/
|
|
44
|
+
export type IInteraction = RegisteredInteraction;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interaction.interface.js","sourceRoot":"","sources":["../../src/lib/interaction.interface.ts"],"names":[],"mappings":"","sourcesContent":["
|
|
1
|
+
{"version":3,"file":"interaction.interface.js","sourceRoot":"","sources":["../../src/lib/interaction.interface.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Smallest shared contract for any form-associated custom element.\n *\n * Keep this intentionally tiny and platform-aligned. QTI-specific concerns\n * such as response identifiers, readonly handling, and validation\n * should be layered via separate capability interfaces below.\n */\nexport interface FormAssociatedInteraction extends HTMLElement {\n disabled: boolean;\n readonly internals: ElementInternals;\n formResetCallback(): void;\n}\n\n/** QTI response identity capability. */\nexport interface ResponseIdentifiedInteraction {\n responseIdentifier: string;\n}\n\n/** Optional readonly capability used by many QTI interactions. */\nexport interface ReadonlyInteraction {\n readonly: boolean;\n}\n\n/** Optional reset capability beyond the platform form reset callback. */\nexport interface ResettableInteraction {\n reset(): void;\n}\n\n/** Interaction that publishes and stores a response value. */\nexport interface ResponseInteractionElement extends FormAssociatedInteraction {\n responseIdentifier: string;\n response: string | string[] | null;\n saveResponse(value: string | string[], state?: string | null): void;\n}\n\n/** Interaction that participates in validity reporting. */\nexport interface ValidatableInteraction extends FormAssociatedInteraction {\n validate(): boolean;\n reportValidity(): boolean;\n}\n\n/**\n * Common contract expected by item containers when tracking registered interactions.\n */\nexport interface RegisteredInteraction\n extends ResponseInteractionElement,\n ValidatableInteraction,\n ReadonlyInteraction,\n ResettableInteraction {}\n\n/**\n * @deprecated Prefer `RegisteredInteraction` for new code.\n */\nexport type IInteraction = RegisteredInteraction;\n"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `response="..."` / `correct-response="..."` attribute codec.
|
|
3
|
+
*
|
|
4
|
+
* Authors put a single string on an interaction element. This module is the
|
|
5
|
+
* single source of truth for how that string parses into the runtime's
|
|
6
|
+
* internal value AND how an editor serializes the internal value back to the
|
|
7
|
+
* attribute. Both `@qti-components/*` interactions and `@citolab/prose-qti`
|
|
8
|
+
* (the editor) import from here — no per-codebase parsers, no drift.
|
|
9
|
+
*
|
|
10
|
+
* The two attributes share the same grammar; the codec is agnostic about
|
|
11
|
+
* which of them is being parsed.
|
|
12
|
+
*
|
|
13
|
+
* ---
|
|
14
|
+
*
|
|
15
|
+
* Format reference (per interaction):
|
|
16
|
+
*
|
|
17
|
+
* | Interaction | base-type | cardinality | attribute value |
|
|
18
|
+
* |----------------------|---------------|----------------------|---------------------------|
|
|
19
|
+
* | choice | identifier | single / multiple | "A" / "A,B,C" |
|
|
20
|
+
* | inline-choice | identifier | single | "Y" |
|
|
21
|
+
* | hottext | identifier | single / multiple | "h1" / "h1,h2" |
|
|
22
|
+
* | hotspot | identifier | single / multiple | "spot1" / "spot1,spot2" |
|
|
23
|
+
* | order | identifier | ordered | "a,b,c" (order matters) |
|
|
24
|
+
* | graphic-order | identifier | ordered | "spot1,spot2,spot3" |
|
|
25
|
+
* | match | directedPair | multiple | "src1 tgt1,src2 tgt2" |
|
|
26
|
+
* | associate | directedPair | multiple | "A B,C D" |
|
|
27
|
+
* | gap-match | directedPair | multiple | "gtext1 gap1,..." |
|
|
28
|
+
* | graphic-associate | directedPair | multiple | "spot1 spot2,..." |
|
|
29
|
+
* | graphic-gap-match | directedPair | multiple | "gimg1 spot1,..." |
|
|
30
|
+
* | select-point | point | single / multiple | "100 150" / "100 150,…" |
|
|
31
|
+
* | slider | integer | single | "50" |
|
|
32
|
+
* | text-entry | string | single | "paris" |
|
|
33
|
+
*
|
|
34
|
+
* Rules:
|
|
35
|
+
* - Comma `,` separates VALUES (different identifiers / pairs / points).
|
|
36
|
+
* - Space ` ` separates PARTS within one value (pair members or point coords).
|
|
37
|
+
* - Each value is trimmed on parse.
|
|
38
|
+
* - No escape mechanism. Identifiers that contain commas cannot round-trip.
|
|
39
|
+
*/
|
|
40
|
+
/** Canonical parsed value of a `response` / `correct-response` attribute. */
|
|
41
|
+
export type ResponseValue = string | string[] | null;
|
|
42
|
+
/** Identifier literal — opaque QTI identifier (e.g. `"choice1"`). */
|
|
43
|
+
export type Identifier = string;
|
|
44
|
+
/** Directed pair literal — `"sourceId targetId"`. Used by match / associate / gap-match. */
|
|
45
|
+
export type DirectedPair = `${string} ${string}`;
|
|
46
|
+
/** Point literal — `"x y"` integer coordinates. Used by select-point / graphic-*. */
|
|
47
|
+
export type Point = `${number} ${number}`;
|
|
48
|
+
/**
|
|
49
|
+
* Parse a raw `response` / `correct-response` attribute string into the
|
|
50
|
+
* canonical value.
|
|
51
|
+
*
|
|
52
|
+
* - `null` / `undefined` / empty → `null`
|
|
53
|
+
* - no comma → trimmed string
|
|
54
|
+
* - one or more commas → trimmed array (single-element collapses to a string)
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseResponseAttribute(raw: string | null | undefined): ResponseValue;
|
|
57
|
+
/**
|
|
58
|
+
* Serialize a canonical value back to the attribute string. Returns `null` for
|
|
59
|
+
* empty inputs so a Lit `@property({ reflect: true })` drops the attribute.
|
|
60
|
+
*/
|
|
61
|
+
export declare function serializeResponseAttribute(value: ResponseValue): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Iterate over the comma-separated entries in a response value, regardless of
|
|
64
|
+
* whether the caller has the raw string, the parsed string, or a string[].
|
|
65
|
+
* Yields trimmed non-empty entries.
|
|
66
|
+
*/
|
|
67
|
+
export declare function iterResponseValues(raw: ResponseValue): Generator<string>;
|
|
68
|
+
/** Parse a directed-pair value into `{ source, target }`. Throws on malformed input. */
|
|
69
|
+
export declare function parsePair(value: string): {
|
|
70
|
+
source: string;
|
|
71
|
+
target: string;
|
|
72
|
+
};
|
|
73
|
+
/** Serialize a directed pair to canonical `"source target"` form. */
|
|
74
|
+
export declare function serializePair(source: string, target: string): DirectedPair;
|
|
75
|
+
/** Parse a point value into `{ x, y }`. Throws on malformed input. */
|
|
76
|
+
export declare function parsePoint(value: string): {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
};
|
|
80
|
+
/** Serialize a point to canonical `"x y"` form. */
|
|
81
|
+
export declare function serializePoint(x: number, y: number): Point;
|
|
82
|
+
/**
|
|
83
|
+
* Ready-to-use Lit `converter` for `response` / `correct-response` attributes.
|
|
84
|
+
* Assign to the `converter` option:
|
|
85
|
+
*
|
|
86
|
+
* @property({ attribute: 'response', converter: responseAttributeConverter({ emptyAs: [] }) })
|
|
87
|
+
*
|
|
88
|
+
* `emptyAs` picks the sentinel returned when the attribute is absent / blank.
|
|
89
|
+
* Three sentinels exist across the codebase — match the one your call site
|
|
90
|
+
* already uses. Future work may normalize to a single sentinel.
|
|
91
|
+
*
|
|
92
|
+
* - '' (empty string) — ChoicesMixin convention
|
|
93
|
+
* - [] — DragDropSlottedMixin / graphic-associate convention
|
|
94
|
+
* - null — base interaction / select-point convention
|
|
95
|
+
*
|
|
96
|
+
* On the serialize side, the `emptyAs` value roundtrips back to `null` so a
|
|
97
|
+
* `reflect: true` property drops the attribute rather than emitting `""`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function responseAttributeConverter<E extends '' | null | readonly unknown[]>(opts: {
|
|
100
|
+
emptyAs: E;
|
|
101
|
+
}): {
|
|
102
|
+
fromAttribute: (value: string | null) => ResponseValue | E;
|
|
103
|
+
toAttribute: (value: ResponseValue | E) => string | null;
|
|
104
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `response="..."` / `correct-response="..."` attribute codec.
|
|
3
|
+
*
|
|
4
|
+
* Authors put a single string on an interaction element. This module is the
|
|
5
|
+
* single source of truth for how that string parses into the runtime's
|
|
6
|
+
* internal value AND how an editor serializes the internal value back to the
|
|
7
|
+
* attribute. Both `@qti-components/*` interactions and `@citolab/prose-qti`
|
|
8
|
+
* (the editor) import from here — no per-codebase parsers, no drift.
|
|
9
|
+
*
|
|
10
|
+
* The two attributes share the same grammar; the codec is agnostic about
|
|
11
|
+
* which of them is being parsed.
|
|
12
|
+
*
|
|
13
|
+
* ---
|
|
14
|
+
*
|
|
15
|
+
* Format reference (per interaction):
|
|
16
|
+
*
|
|
17
|
+
* | Interaction | base-type | cardinality | attribute value |
|
|
18
|
+
* |----------------------|---------------|----------------------|---------------------------|
|
|
19
|
+
* | choice | identifier | single / multiple | "A" / "A,B,C" |
|
|
20
|
+
* | inline-choice | identifier | single | "Y" |
|
|
21
|
+
* | hottext | identifier | single / multiple | "h1" / "h1,h2" |
|
|
22
|
+
* | hotspot | identifier | single / multiple | "spot1" / "spot1,spot2" |
|
|
23
|
+
* | order | identifier | ordered | "a,b,c" (order matters) |
|
|
24
|
+
* | graphic-order | identifier | ordered | "spot1,spot2,spot3" |
|
|
25
|
+
* | match | directedPair | multiple | "src1 tgt1,src2 tgt2" |
|
|
26
|
+
* | associate | directedPair | multiple | "A B,C D" |
|
|
27
|
+
* | gap-match | directedPair | multiple | "gtext1 gap1,..." |
|
|
28
|
+
* | graphic-associate | directedPair | multiple | "spot1 spot2,..." |
|
|
29
|
+
* | graphic-gap-match | directedPair | multiple | "gimg1 spot1,..." |
|
|
30
|
+
* | select-point | point | single / multiple | "100 150" / "100 150,…" |
|
|
31
|
+
* | slider | integer | single | "50" |
|
|
32
|
+
* | text-entry | string | single | "paris" |
|
|
33
|
+
*
|
|
34
|
+
* Rules:
|
|
35
|
+
* - Comma `,` separates VALUES (different identifiers / pairs / points).
|
|
36
|
+
* - Space ` ` separates PARTS within one value (pair members or point coords).
|
|
37
|
+
* - Each value is trimmed on parse.
|
|
38
|
+
* - No escape mechanism. Identifiers that contain commas cannot round-trip.
|
|
39
|
+
*/
|
|
40
|
+
// ─── attribute-level parser / serializer ─────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Parse a raw `response` / `correct-response` attribute string into the
|
|
43
|
+
* canonical value.
|
|
44
|
+
*
|
|
45
|
+
* - `null` / `undefined` / empty → `null`
|
|
46
|
+
* - no comma → trimmed string
|
|
47
|
+
* - one or more commas → trimmed array (single-element collapses to a string)
|
|
48
|
+
*/
|
|
49
|
+
export function parseResponseAttribute(raw) {
|
|
50
|
+
if (raw == null)
|
|
51
|
+
return null;
|
|
52
|
+
if (!raw.includes(',')) {
|
|
53
|
+
const trimmed = raw.trim();
|
|
54
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
55
|
+
}
|
|
56
|
+
const tokens = raw
|
|
57
|
+
.split(',')
|
|
58
|
+
.map(token => token.trim())
|
|
59
|
+
.filter(token => token.length > 0);
|
|
60
|
+
if (tokens.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
if (tokens.length === 1)
|
|
63
|
+
return tokens[0];
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Serialize a canonical value back to the attribute string. Returns `null` for
|
|
68
|
+
* empty inputs so a Lit `@property({ reflect: true })` drops the attribute.
|
|
69
|
+
*/
|
|
70
|
+
export function serializeResponseAttribute(value) {
|
|
71
|
+
if (value == null)
|
|
72
|
+
return null;
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
const tokens = value.map(token => token.trim()).filter(token => token.length > 0);
|
|
75
|
+
return tokens.length > 0 ? tokens.join(',') : null;
|
|
76
|
+
}
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
79
|
+
}
|
|
80
|
+
// ─── value-shape helpers (per interaction type) ──────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Iterate over the comma-separated entries in a response value, regardless of
|
|
83
|
+
* whether the caller has the raw string, the parsed string, or a string[].
|
|
84
|
+
* Yields trimmed non-empty entries.
|
|
85
|
+
*/
|
|
86
|
+
export function* iterResponseValues(raw) {
|
|
87
|
+
if (raw == null)
|
|
88
|
+
return;
|
|
89
|
+
if (Array.isArray(raw)) {
|
|
90
|
+
for (const v of raw) {
|
|
91
|
+
const t = v.trim();
|
|
92
|
+
if (t)
|
|
93
|
+
yield t;
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!raw.includes(',')) {
|
|
98
|
+
const t = raw.trim();
|
|
99
|
+
if (t)
|
|
100
|
+
yield t;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const v of raw.split(',')) {
|
|
104
|
+
const t = v.trim();
|
|
105
|
+
if (t)
|
|
106
|
+
yield t;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Parse a directed-pair value into `{ source, target }`. Throws on malformed input. */
|
|
110
|
+
export function parsePair(value) {
|
|
111
|
+
const [source, target, ...rest] = value.trim().split(/\s+/);
|
|
112
|
+
if (!source || !target || rest.length > 0) {
|
|
113
|
+
throw new Error(`malformed directed-pair value: "${value}" (expected "source target")`);
|
|
114
|
+
}
|
|
115
|
+
return { source, target };
|
|
116
|
+
}
|
|
117
|
+
/** Serialize a directed pair to canonical `"source target"` form. */
|
|
118
|
+
export function serializePair(source, target) {
|
|
119
|
+
return `${source} ${target}`;
|
|
120
|
+
}
|
|
121
|
+
/** Parse a point value into `{ x, y }`. Throws on malformed input. */
|
|
122
|
+
export function parsePoint(value) {
|
|
123
|
+
const [xStr, yStr, ...rest] = value.trim().split(/\s+/);
|
|
124
|
+
const x = Number(xStr);
|
|
125
|
+
const y = Number(yStr);
|
|
126
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || rest.length > 0) {
|
|
127
|
+
throw new Error(`malformed point value: "${value}" (expected "x y" numeric coords)`);
|
|
128
|
+
}
|
|
129
|
+
return { x, y };
|
|
130
|
+
}
|
|
131
|
+
/** Serialize a point to canonical `"x y"` form. */
|
|
132
|
+
export function serializePoint(x, y) {
|
|
133
|
+
return `${x} ${y}`;
|
|
134
|
+
}
|
|
135
|
+
// ─── Lit converter factory ───────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Ready-to-use Lit `converter` for `response` / `correct-response` attributes.
|
|
138
|
+
* Assign to the `converter` option:
|
|
139
|
+
*
|
|
140
|
+
* @property({ attribute: 'response', converter: responseAttributeConverter({ emptyAs: [] }) })
|
|
141
|
+
*
|
|
142
|
+
* `emptyAs` picks the sentinel returned when the attribute is absent / blank.
|
|
143
|
+
* Three sentinels exist across the codebase — match the one your call site
|
|
144
|
+
* already uses. Future work may normalize to a single sentinel.
|
|
145
|
+
*
|
|
146
|
+
* - '' (empty string) — ChoicesMixin convention
|
|
147
|
+
* - [] — DragDropSlottedMixin / graphic-associate convention
|
|
148
|
+
* - null — base interaction / select-point convention
|
|
149
|
+
*
|
|
150
|
+
* On the serialize side, the `emptyAs` value roundtrips back to `null` so a
|
|
151
|
+
* `reflect: true` property drops the attribute rather than emitting `""`.
|
|
152
|
+
*/
|
|
153
|
+
export function responseAttributeConverter(opts) {
|
|
154
|
+
const { emptyAs } = opts;
|
|
155
|
+
const isEmptySentinel = (value) => value === emptyAs || value == null || value === '' || (Array.isArray(value) && value.length === 0);
|
|
156
|
+
return {
|
|
157
|
+
fromAttribute: (value) => {
|
|
158
|
+
const parsed = parseResponseAttribute(value);
|
|
159
|
+
return parsed === null ? emptyAs : parsed;
|
|
160
|
+
},
|
|
161
|
+
toAttribute: (value) => {
|
|
162
|
+
if (isEmptySentinel(value))
|
|
163
|
+
return null;
|
|
164
|
+
return serializeResponseAttribute(value);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/lib/response.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canonical `response=\"...\"` / `correct-response=\"...\"` attribute codec.\n *\n * Authors put a single string on an interaction element. This module is the\n * single source of truth for how that string parses into the runtime's\n * internal value AND how an editor serializes the internal value back to the\n * attribute. Both `@qti-components/*` interactions and `@citolab/prose-qti`\n * (the editor) import from here — no per-codebase parsers, no drift.\n *\n * The two attributes share the same grammar; the codec is agnostic about\n * which of them is being parsed.\n *\n * ---\n *\n * Format reference (per interaction):\n *\n * | Interaction | base-type | cardinality | attribute value |\n * |----------------------|---------------|----------------------|---------------------------|\n * | choice | identifier | single / multiple | \"A\" / \"A,B,C\" |\n * | inline-choice | identifier | single | \"Y\" |\n * | hottext | identifier | single / multiple | \"h1\" / \"h1,h2\" |\n * | hotspot | identifier | single / multiple | \"spot1\" / \"spot1,spot2\" |\n * | order | identifier | ordered | \"a,b,c\" (order matters) |\n * | graphic-order | identifier | ordered | \"spot1,spot2,spot3\" |\n * | match | directedPair | multiple | \"src1 tgt1,src2 tgt2\" |\n * | associate | directedPair | multiple | \"A B,C D\" |\n * | gap-match | directedPair | multiple | \"gtext1 gap1,...\" |\n * | graphic-associate | directedPair | multiple | \"spot1 spot2,...\" |\n * | graphic-gap-match | directedPair | multiple | \"gimg1 spot1,...\" |\n * | select-point | point | single / multiple | \"100 150\" / \"100 150,…\" |\n * | slider | integer | single | \"50\" |\n * | text-entry | string | single | \"paris\" |\n *\n * Rules:\n * - Comma `,` separates VALUES (different identifiers / pairs / points).\n * - Space ` ` separates PARTS within one value (pair members or point coords).\n * - Each value is trimmed on parse.\n * - No escape mechanism. Identifiers that contain commas cannot round-trip.\n */\n\n// ─── parsed shape ────────────────────────────────────────────────────────────\n\n/** Canonical parsed value of a `response` / `correct-response` attribute. */\nexport type ResponseValue = string | string[] | null;\n\n// ─── per-shape branded types (advisory at compile time) ──────────────────────\n\n/** Identifier literal — opaque QTI identifier (e.g. `\"choice1\"`). */\nexport type Identifier = string;\n\n/** Directed pair literal — `\"sourceId targetId\"`. Used by match / associate / gap-match. */\nexport type DirectedPair = `${string} ${string}`;\n\n/** Point literal — `\"x y\"` integer coordinates. Used by select-point / graphic-*. */\nexport type Point = `${number} ${number}`;\n\n// ─── attribute-level parser / serializer ─────────────────────────────────────\n\n/**\n * Parse a raw `response` / `correct-response` attribute string into the\n * canonical value.\n *\n * - `null` / `undefined` / empty → `null`\n * - no comma → trimmed string\n * - one or more commas → trimmed array (single-element collapses to a string)\n */\nexport function parseResponseAttribute(raw: string | null | undefined): ResponseValue {\n if (raw == null) return null;\n if (!raw.includes(',')) {\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n }\n const tokens = raw\n .split(',')\n .map(token => token.trim())\n .filter(token => token.length > 0);\n if (tokens.length === 0) return null;\n if (tokens.length === 1) return tokens[0];\n return tokens;\n}\n\n/**\n * Serialize a canonical value back to the attribute string. Returns `null` for\n * empty inputs so a Lit `@property({ reflect: true })` drops the attribute.\n */\nexport function serializeResponseAttribute(value: ResponseValue): string | null {\n if (value == null) return null;\n if (Array.isArray(value)) {\n const tokens = value.map(token => token.trim()).filter(token => token.length > 0);\n return tokens.length > 0 ? tokens.join(',') : null;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n// ─── value-shape helpers (per interaction type) ──────────────────────────────\n\n/**\n * Iterate over the comma-separated entries in a response value, regardless of\n * whether the caller has the raw string, the parsed string, or a string[].\n * Yields trimmed non-empty entries.\n */\nexport function* iterResponseValues(raw: ResponseValue): Generator<string> {\n if (raw == null) return;\n if (Array.isArray(raw)) {\n for (const v of raw) {\n const t = v.trim();\n if (t) yield t;\n }\n return;\n }\n if (!raw.includes(',')) {\n const t = raw.trim();\n if (t) yield t;\n return;\n }\n for (const v of raw.split(',')) {\n const t = v.trim();\n if (t) yield t;\n }\n}\n\n/** Parse a directed-pair value into `{ source, target }`. Throws on malformed input. */\nexport function parsePair(value: string): { source: string; target: string } {\n const [source, target, ...rest] = value.trim().split(/\\s+/);\n if (!source || !target || rest.length > 0) {\n throw new Error(`malformed directed-pair value: \"${value}\" (expected \"source target\")`);\n }\n return { source, target };\n}\n\n/** Serialize a directed pair to canonical `\"source target\"` form. */\nexport function serializePair(source: string, target: string): DirectedPair {\n return `${source} ${target}`;\n}\n\n/** Parse a point value into `{ x, y }`. Throws on malformed input. */\nexport function parsePoint(value: string): { x: number; y: number } {\n const [xStr, yStr, ...rest] = value.trim().split(/\\s+/);\n const x = Number(xStr);\n const y = Number(yStr);\n if (!Number.isFinite(x) || !Number.isFinite(y) || rest.length > 0) {\n throw new Error(`malformed point value: \"${value}\" (expected \"x y\" numeric coords)`);\n }\n return { x, y };\n}\n\n/** Serialize a point to canonical `\"x y\"` form. */\nexport function serializePoint(x: number, y: number): Point {\n return `${x} ${y}`;\n}\n\n// ─── Lit converter factory ───────────────────────────────────────────────────\n\n/**\n * Ready-to-use Lit `converter` for `response` / `correct-response` attributes.\n * Assign to the `converter` option:\n *\n * @property({ attribute: 'response', converter: responseAttributeConverter({ emptyAs: [] }) })\n *\n * `emptyAs` picks the sentinel returned when the attribute is absent / blank.\n * Three sentinels exist across the codebase — match the one your call site\n * already uses. Future work may normalize to a single sentinel.\n *\n * - '' (empty string) — ChoicesMixin convention\n * - [] — DragDropSlottedMixin / graphic-associate convention\n * - null — base interaction / select-point convention\n *\n * On the serialize side, the `emptyAs` value roundtrips back to `null` so a\n * `reflect: true` property drops the attribute rather than emitting `\"\"`.\n */\nexport function responseAttributeConverter<E extends '' | null | readonly unknown[]>(opts: {\n emptyAs: E;\n}): {\n fromAttribute: (value: string | null) => ResponseValue | E;\n toAttribute: (value: ResponseValue | E) => string | null;\n} {\n const { emptyAs } = opts;\n const isEmptySentinel = (value: unknown): boolean =>\n value === emptyAs || value == null || value === '' || (Array.isArray(value) && value.length === 0);\n\n return {\n fromAttribute: (value: string | null): ResponseValue | E => {\n const parsed = parseResponseAttribute(value);\n return parsed === null ? emptyAs : parsed;\n },\n toAttribute: (value: ResponseValue | E): string | null => {\n if (isEmptySentinel(value)) return null;\n return serializeResponseAttribute(value as ResponseValue);\n }\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/register.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,eAAe,EAAE,CAAC;IAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;AACH,CAAC","sourcesContent":["import { qtiBaseElements } from './elements';\n\nfor (const { tag, ctor } of qtiBaseElements) {\n if (!customElements.get(tag)) {\n customElements.define(tag, ctor);\n }\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The border-box reset, for shadow trees.
|
|
3
|
+
*
|
|
4
|
+
* `modern-normalize` applies `*, ::before, ::after { box-sizing: border-box }` to the document,
|
|
5
|
+
* but a universal selector never crosses a shadow boundary: every shadow root starts over at
|
|
6
|
+
* the UA default, `content-box`. So each component has to opt in, and until now 15 of them did
|
|
7
|
+
* so ad hoc — one of them with `content-box`, one with `!important`.
|
|
8
|
+
*
|
|
9
|
+
* Why this matters beyond tidiness: under `content-box`, padding and border are *size*. A rule
|
|
10
|
+
* that only means to repaint a chip — `:state(dragging) { border: none }` — silently shrinks it
|
|
11
|
+
* by two pixels per axis, the drag bank rewraps, and a drop target slides out from under the
|
|
12
|
+
* user's pointer mid-drag. Border-box is what makes "a state may repaint but never re-lay-out"
|
|
13
|
+
* a rule the CSS can actually keep.
|
|
14
|
+
*
|
|
15
|
+
* `:host` is set explicitly and descendants inherit, so a component that genuinely needs
|
|
16
|
+
* `content-box` for a subtree (an image map, say) can opt out at one node and have it hold for
|
|
17
|
+
* everything below it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const boxSizing: import("lit").CSSResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* The border-box reset, for shadow trees.
|
|
4
|
+
*
|
|
5
|
+
* `modern-normalize` applies `*, ::before, ::after { box-sizing: border-box }` to the document,
|
|
6
|
+
* but a universal selector never crosses a shadow boundary: every shadow root starts over at
|
|
7
|
+
* the UA default, `content-box`. So each component has to opt in, and until now 15 of them did
|
|
8
|
+
* so ad hoc — one of them with `content-box`, one with `!important`.
|
|
9
|
+
*
|
|
10
|
+
* Why this matters beyond tidiness: under `content-box`, padding and border are *size*. A rule
|
|
11
|
+
* that only means to repaint a chip — `:state(dragging) { border: none }` — silently shrinks it
|
|
12
|
+
* by two pixels per axis, the drag bank rewraps, and a drop target slides out from under the
|
|
13
|
+
* user's pointer mid-drag. Border-box is what makes "a state may repaint but never re-lay-out"
|
|
14
|
+
* a rule the CSS can actually keep.
|
|
15
|
+
*
|
|
16
|
+
* `:host` is set explicitly and descendants inherit, so a component that genuinely needs
|
|
17
|
+
* `content-box` for a subtree (an image map, say) can opt out at one node and have it hold for
|
|
18
|
+
* everything below it.
|
|
19
|
+
*/
|
|
20
|
+
export const boxSizing = css `
|
|
21
|
+
:host {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
*,
|
|
26
|
+
*::before,
|
|
27
|
+
*::after {
|
|
28
|
+
box-sizing: inherit;
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
//# sourceMappingURL=box-sizing.styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"box-sizing.styles.js","sourceRoot":"","sources":["../../src/styles/box-sizing.styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAA;;;;;;;;;;CAU3B,CAAC","sourcesContent":["import { css } from 'lit';\n\n/**\n * The border-box reset, for shadow trees.\n *\n * `modern-normalize` applies `*, ::before, ::after { box-sizing: border-box }` to the document,\n * but a universal selector never crosses a shadow boundary: every shadow root starts over at\n * the UA default, `content-box`. So each component has to opt in, and until now 15 of them did\n * so ad hoc — one of them with `content-box`, one with `!important`.\n *\n * Why this matters beyond tidiness: under `content-box`, padding and border are *size*. A rule\n * that only means to repaint a chip — `:state(dragging) { border: none }` — silently shrinks it\n * by two pixels per axis, the drag bank rewraps, and a drop target slides out from under the\n * user's pointer mid-drag. Border-box is what makes \"a state may repaint but never re-lay-out\"\n * a rule the CSS can actually keep.\n *\n * `:host` is set explicitly and descendants inherit, so a component that genuinely needs\n * `content-box` for a subtree (an image map, say) can opt out at one node and have it hold for\n * everything below it.\n */\nexport const boxSizing = css`\n :host {\n box-sizing: border-box;\n }\n\n *,\n *::before,\n *::after {\n box-sizing: inherit;\n }\n`;\n"]}
|